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,112 @@
|
|
|
1
|
+
"""The PEN-STACK agent: tool-use orchestration.
|
|
2
|
+
|
|
3
|
+
Given a natural-language goal ("durably express factor IX in hepatocytes"), the agent plans the whole
|
|
4
|
+
write by calling validated tools (writability -> reachable writers -> writer axes -> plan_write -> cited
|
|
5
|
+
literature) in a tool-calling loop driven by the configured LLM (hybrid: NVIDIA Nemotron with Ollama
|
|
6
|
+
fallback, via ``pen_stack.rag.llm.chat``). Guardrails: it obtains numbers ONLY from tool calls (no
|
|
7
|
+
free-text predictions), refuses clinical-directive prompts, and logs an auditable trace.
|
|
8
|
+
|
|
9
|
+
Graceful: if no LLM provider is reachable, ``run_agent`` returns a refusal-free deterministic fallback
|
|
10
|
+
that calls plan_write directly - so the platform degrades to the validated pipeline rather than failing.
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
from pen_stack.agent.guardrails import DISCLAIMER, out_of_scope
|
|
18
|
+
from pen_stack.agent.tools import SCHEMAS, dispatch
|
|
19
|
+
from pen_stack.rag.llm import chat as llm_chat
|
|
20
|
+
|
|
21
|
+
_TRACES = Path(__file__).resolve().parents[2] / "out" / "agent_traces"
|
|
22
|
+
|
|
23
|
+
_SYSTEM = (
|
|
24
|
+
"You are the PEN-STACK genome-writing planning agent. You MUST obtain every fact and number by "
|
|
25
|
+
"calling the provided tools - never invent a number, gene, score, or citation. Plan a write by "
|
|
26
|
+
"calling: writability, reachable_writers, writer_axes, plan_write, ask_literature. When you have "
|
|
27
|
+
"enough tool results, write a short plan that cites which tool produced each number. Decision-support "
|
|
28
|
+
"only - never give clinical directives.")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _tool_response(style: str, call_id: str | None, content: str) -> dict:
|
|
32
|
+
"""Format a tool-result message for the provider's API style."""
|
|
33
|
+
if style == "anthropic": # Anthropic returns tool results as a user-turn tool_result content block
|
|
34
|
+
block = {"type": "tool_result", "content": content}
|
|
35
|
+
if call_id is not None:
|
|
36
|
+
block["tool_use_id"] = call_id
|
|
37
|
+
return {"role": "user", "content": [block]}
|
|
38
|
+
m = {"role": "tool", "content": content}
|
|
39
|
+
if style == "openai" and call_id is not None:
|
|
40
|
+
m["tool_call_id"] = call_id
|
|
41
|
+
return m
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def run_agent(goal: str, max_steps: int = 12, cfg: dict | None = None) -> dict:
|
|
45
|
+
"""Turn a goal into a cited, auditable plan. Numbers come only from tool calls."""
|
|
46
|
+
refusal = out_of_scope(goal)
|
|
47
|
+
if refusal:
|
|
48
|
+
return {"refused": True, "plan": refusal, "trace": [], "disclaimer": DISCLAIMER}
|
|
49
|
+
|
|
50
|
+
from pen_stack.rag.llm import load_llm_config
|
|
51
|
+
step_timeout = int((cfg or load_llm_config()).get("agent_call_timeout", 60))
|
|
52
|
+
msgs = [{"role": "system", "content": _SYSTEM}, {"role": "user", "content": goal}]
|
|
53
|
+
trace: list[dict] = []
|
|
54
|
+
seen: set = set()
|
|
55
|
+
|
|
56
|
+
for _ in range(max_steps):
|
|
57
|
+
resp = llm_chat(msgs, tools=SCHEMAS, cfg=cfg, timeout=step_timeout)
|
|
58
|
+
if resp is None:
|
|
59
|
+
return _fallback(goal, trace)
|
|
60
|
+
provider, style = resp.get("provider"), resp.get("style", "openai")
|
|
61
|
+
calls = resp.get("tool_calls") or []
|
|
62
|
+
if not calls:
|
|
63
|
+
return {"refused": False, "plan": resp.get("content", "").strip(),
|
|
64
|
+
"trace": trace, "disclaimer": DISCLAIMER, "llm": True, "provider": provider}
|
|
65
|
+
msgs.append(resp["raw"]) # append the assistant turn verbatim
|
|
66
|
+
raw_calls = resp["raw"].get("tool_calls") or []
|
|
67
|
+
for i, c in enumerate(calls):
|
|
68
|
+
name = c["function"]["name"]
|
|
69
|
+
args = c["function"]["arguments"]
|
|
70
|
+
# anthropic carries the id on the normalised call; openai carries it on the raw tool_call
|
|
71
|
+
call_id = c.get("id") or (raw_calls[i].get("id") if i < len(raw_calls) else None)
|
|
72
|
+
key = f"{name}:{json.dumps(args, sort_keys=True, default=str)}"
|
|
73
|
+
if key in seen:
|
|
74
|
+
msgs.append(_tool_response(style, call_id, json.dumps(
|
|
75
|
+
{"note": "already called with these args; use prior result and finalise the plan"})))
|
|
76
|
+
continue
|
|
77
|
+
seen.add(key)
|
|
78
|
+
try:
|
|
79
|
+
result = dispatch(name, args) # VALIDATED tool only
|
|
80
|
+
except Exception as e: # noqa: BLE001
|
|
81
|
+
result = {"error": str(e)}
|
|
82
|
+
trace.append({"tool": name, "args": args, "result": result})
|
|
83
|
+
msgs.append(_tool_response(style, call_id, json.dumps(result, default=str)))
|
|
84
|
+
return {"refused": False, "plan": "(max steps reached)", "trace": trace,
|
|
85
|
+
"disclaimer": DISCLAIMER, "llm": True}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _fallback(goal: str, trace: list[dict]) -> dict:
|
|
89
|
+
"""Deterministic fallback when no LLM is reachable: call plan_write on a best-effort parse."""
|
|
90
|
+
from pen_stack.planner.optimize import EditIntent
|
|
91
|
+
gene = next((w for w in goal.replace(",", " ").split() if w.isupper() and len(w) >= 2), None)
|
|
92
|
+
intent = EditIntent.SAFE_HARBOUR.value
|
|
93
|
+
for kw, it in [("disrupt", "knock_in_with_disruption"), ("knock", "knock_in_with_disruption"),
|
|
94
|
+
("durab", "high_durability_insertion"), ("enhancer", "regulatory_excision"),
|
|
95
|
+
("repeat", "repeat_excision")]:
|
|
96
|
+
if kw in goal.lower():
|
|
97
|
+
intent = it
|
|
98
|
+
break
|
|
99
|
+
if not gene:
|
|
100
|
+
return {"refused": False, "plan": "No target gene detected; LLM unavailable.",
|
|
101
|
+
"trace": trace, "disclaimer": DISCLAIMER, "llm": False}
|
|
102
|
+
res = dispatch("plan_write", {"gene": gene, "intent": intent})
|
|
103
|
+
trace.append({"tool": "plan_write", "args": {"gene": gene, "intent": intent}, "result": res})
|
|
104
|
+
return {"refused": False, "plan": f"[deterministic fallback] plan for {gene} ({intent})",
|
|
105
|
+
"trace": trace, "disclaimer": DISCLAIMER, "llm": False}
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def save_trace(result: dict, name: str) -> Path:
|
|
109
|
+
_TRACES.mkdir(parents=True, exist_ok=True)
|
|
110
|
+
p = _TRACES / f"{name}.json"
|
|
111
|
+
p.write_text(json.dumps(result, indent=2, default=str), encoding="utf-8")
|
|
112
|
+
return p
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Live foundation-model orchestration.
|
|
2
|
+
|
|
3
|
+
A reasoning loop that GENERATES grounded candidates, calls oracles (cache-first / replayable) for a critique
|
|
4
|
+
signal, and disposes via `verify()` (safety + legality + immune). The agent picks WHICH oracle to call; the
|
|
5
|
+
NUMBER always comes from the oracle/tool, never invented. Live calls are cache-keyed and version-pinned, so a
|
|
6
|
+
seed-locked replay reproduces a run from the committed cache (replay is the CI default).
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _oracle_critique(design: dict, *, seed: int = 0) -> dict:
|
|
14
|
+
"""A grounded critique number for the top candidate, sourced from an oracle adapter (cache-first). When the
|
|
15
|
+
candidate carries a writer sequence, use the structure-consistency oracle; otherwise abstain (no fabricated
|
|
16
|
+
value). The returned value/uncertainty come from the OracleResult, never the agent."""
|
|
17
|
+
seq = design.get("writer_candidate_seq") or design.get("writer_seq")
|
|
18
|
+
if not seq:
|
|
19
|
+
return {"oracle": None, "value": None, "available": False, "note": "no sequence supplied; abstaining"}
|
|
20
|
+
from pen_stack.oracles.structure import consistency
|
|
21
|
+
r = consistency(seq)
|
|
22
|
+
return {"oracle": "structure.consistency", "value": (r.value or {}),
|
|
23
|
+
"available": bool(r.available), "cached": bool(getattr(r, "cached", False)),
|
|
24
|
+
"source": getattr(r, "source", None), "output_kind": r.output_kind}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def orchestrate(goal: dict, *, candidates: list[dict] | None = None, max_rounds: int = 4,
|
|
28
|
+
seed: int = 0) -> dict[str, Any]:
|
|
29
|
+
"""Plan -> generate grounded candidates -> call an oracle (cache-first) -> critique via verify() -> refine.
|
|
30
|
+
Returns the chosen design + a trace in which every number is tool-sourced. Deterministic given the inputs +
|
|
31
|
+
seed (replayable from cache). No stage fabricates a value."""
|
|
32
|
+
from pen_stack.design.generate import generate_designs
|
|
33
|
+
from pen_stack.verify import verify
|
|
34
|
+
|
|
35
|
+
state: dict[str, Any] = {"goal": goal, "design": None, "candidates": []}
|
|
36
|
+
trace: list[dict] = []
|
|
37
|
+
for r in range(max_rounds):
|
|
38
|
+
cands = generate_designs(goal, candidates=candidates, keep=5, actor="orchestrator")
|
|
39
|
+
if not cands:
|
|
40
|
+
trace.append({"round": r, "action": "generate", "n": 0,
|
|
41
|
+
"note": "no surviving candidates (atlas absent or all discarded by the discriminator)"})
|
|
42
|
+
break
|
|
43
|
+
top = cands[0]
|
|
44
|
+
oracle = _oracle_critique(top, seed=seed)
|
|
45
|
+
v = verify(dict(top), actor="orchestrator")
|
|
46
|
+
trace.append({"round": r, "action": "generate+oracle+verify", "n": len(cands),
|
|
47
|
+
"oracle": oracle, "verdict": v.summary(), "legal": v.legal,
|
|
48
|
+
"safety": (v.safety.decision if v.safety is not None else None),
|
|
49
|
+
"confidence": v.confidence})
|
|
50
|
+
state["design"], state["candidates"] = top, cands
|
|
51
|
+
if v.legal is True and v.safety is not None and v.safety.decision in ("clear", "flag"):
|
|
52
|
+
break
|
|
53
|
+
# refine: on a non-passing round, narrow the pool to the survivors for the next iteration
|
|
54
|
+
candidates = cands
|
|
55
|
+
return {"goal": goal, "design": state["design"], "trace": trace,
|
|
56
|
+
"no_fabrication": True, "replayable": True}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
"""PEN-Agent - grounded write-planning state machine.
|
|
2
|
+
|
|
3
|
+
A deterministic task-state machine over the VALIDATED tools. It sequences a genome-write plan:
|
|
4
|
+
|
|
5
|
+
goal intake -> site selection (writability) -> writer selection (reachability) ->
|
|
6
|
+
cargo design (+ Cargo Polish) -> off-target -> 3D structural risk -> report
|
|
7
|
+
|
|
8
|
+
Core property (the contribution): NO FABRICATION. Every number in the output is copied verbatim from a
|
|
9
|
+
tool-result dict and tagged with that tool's provenance; a step whose tool cannot ground a value is marked
|
|
10
|
+
`degraded`/`refused`, never invented. The agent therefore runs end-to-end even when AlphaGenome or the
|
|
11
|
+
bridge engine is unavailable - those steps degrade with a reason instead of guessing.
|
|
12
|
+
|
|
13
|
+
The LLM (agent/orchestrator.py) is an optional conversational front-end over this same machine; the plan
|
|
14
|
+
itself is deterministic, so the result is reproducible and the no-fabrication guarantee holds with or
|
|
15
|
+
without an LLM. Modes: "automatic" (run all steps), "guided" (stop after each step), "qa" (single tool).
|
|
16
|
+
"""
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from dataclasses import dataclass, field
|
|
20
|
+
|
|
21
|
+
from pen_stack.agent import tools as T
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class Step:
|
|
26
|
+
name: str
|
|
27
|
+
tool: str | None
|
|
28
|
+
status: str # ok | degraded | refused
|
|
29
|
+
provenance: str | None = None
|
|
30
|
+
result: dict = field(default_factory=dict)
|
|
31
|
+
reason: str | None = None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _site_selection(gene: str, ct: str) -> Step:
|
|
35
|
+
try:
|
|
36
|
+
r = T.writability(gene, ct)
|
|
37
|
+
except Exception as e: # noqa: BLE001 - missing atlas -> refuse, never fabricate
|
|
38
|
+
return Step("site_selection", "wgenome.writability", "refused", reason=f"{type(e).__name__}: {e}")
|
|
39
|
+
if not r.get("found"):
|
|
40
|
+
return Step("site_selection", r.get("tool"), "refused", reason=f"no writable locus for {gene}")
|
|
41
|
+
return Step("site_selection", r["tool"], "ok", provenance=r["tool"],
|
|
42
|
+
result={"max_writability": r["max_writability"], "safety": r["safety"],
|
|
43
|
+
"p_durable": r["p_durable"], "n_bins": r["n_bins"]})
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _writer_selection(gene: str, intent: str, cargo_bp: int, ct: str) -> tuple[Step, dict]:
|
|
47
|
+
try:
|
|
48
|
+
plan = T.plan_write(gene, intent, cargo_bp, ct)
|
|
49
|
+
except Exception as e: # noqa: BLE001 - missing atlas -> refuse, never fabricate
|
|
50
|
+
return Step("writer_selection", "planner.pipeline", "refused",
|
|
51
|
+
reason=f"{type(e).__name__}: {e}"), {}
|
|
52
|
+
if not plan or plan.get("found") is False:
|
|
53
|
+
return Step("writer_selection", "planner.pipeline", "refused",
|
|
54
|
+
reason="planner returned no plan"), {}
|
|
55
|
+
fam = plan.get("writer") or plan.get("writer_family") or plan.get("family")
|
|
56
|
+
return (Step("writer_selection", "planner.pipeline", "ok", provenance="planner.pipeline",
|
|
57
|
+
result={"writer_family": fam, "score": plan.get("score"),
|
|
58
|
+
"site": {k: plan.get(k) for k in ("chrom", "bin") if k in plan}}),
|
|
59
|
+
plan)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _cargo_design(plan: dict, cargo_bp: int, ct: str, payload_seq: str | None) -> Step:
|
|
63
|
+
from pen_stack.planner.cargo import design_cargo
|
|
64
|
+
fam = plan.get("writer") or plan.get("writer_family") or plan.get("family")
|
|
65
|
+
wr = {"family": fam, "cargo_capacity_bp": plan.get("cargo_capacity_bp"),
|
|
66
|
+
"deliv_class": plan.get("deliv_class")}
|
|
67
|
+
site = (plan.get("chrom"), plan.get("bin"))
|
|
68
|
+
cargo = design_cargo(cargo_bp, wr, site, ct, payload_seq=payload_seq)
|
|
69
|
+
res = {"assembled_bp": cargo["assembled_bp"], "size_ok": cargo["size_ok"]}
|
|
70
|
+
if "cargo_polish" in cargo:
|
|
71
|
+
res["cargo_durability_risk"] = cargo["cargo_polish"]["cargo_durability_risk"]
|
|
72
|
+
res["cargo_band"] = cargo["cargo_polish"]["band"]
|
|
73
|
+
res["cargo_suggestions"] = [f["suggestion"] for f in cargo["cargo_polish"]["flags"]]
|
|
74
|
+
return Step("cargo_design", "planner.cargo", "ok", provenance="planner.cargo+cargo_polish", result=res)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _offtarget(plan: dict) -> Step:
|
|
78
|
+
fam = plan.get("writer") or plan.get("writer_family") or plan.get("family") or ""
|
|
79
|
+
if "bridge" not in str(fam).lower() and "seek" not in str(fam).lower():
|
|
80
|
+
return Step("offtarget", None, "degraded", reason=f"off-target engine applies to bridge/seek writers, not {fam}")
|
|
81
|
+
try:
|
|
82
|
+
from pen_stack.bridge.offtarget import predict_offtargets
|
|
83
|
+
r = predict_offtargets(fam, (plan.get("chrom"), plan.get("bin")))
|
|
84
|
+
except Exception as e: # noqa: BLE001
|
|
85
|
+
return Step("offtarget", "bridge.offtarget", "degraded", reason=f"{type(e).__name__}: {e}")
|
|
86
|
+
if isinstance(r, dict) and r.get("status", "").startswith("pending"):
|
|
87
|
+
return Step("offtarget", "bridge.offtarget", "degraded", reason=r.get("note"))
|
|
88
|
+
return Step("offtarget", "bridge.offtarget", "ok", provenance="bridge.offtarget",
|
|
89
|
+
result=r if isinstance(r, dict) else {"offtargets": r})
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _structural_risk(plan: dict) -> Step:
|
|
93
|
+
chrom, b = plan.get("chrom"), plan.get("bin")
|
|
94
|
+
if chrom is None or b is None:
|
|
95
|
+
return Step("structural_risk", None, "degraded", reason="no concrete site coordinates")
|
|
96
|
+
try:
|
|
97
|
+
from pen_stack.wgenome.structure3d import structural_risk
|
|
98
|
+
r = structural_risk(chrom, int(b) * 1000, int(b) * 1000 + 135_000, offline=True)
|
|
99
|
+
except Exception as e: # noqa: BLE001
|
|
100
|
+
return Step("structural_risk", "wgenome.structure3d", "degraded", reason=f"{type(e).__name__}: {e}")
|
|
101
|
+
if not r.get("available"):
|
|
102
|
+
return Step("structural_risk", "wgenome.structure3d", "degraded",
|
|
103
|
+
reason="AlphaGenome contact map not cached (offline); flag with confidence")
|
|
104
|
+
return Step("structural_risk", "wgenome.structure3d", "ok", provenance="wgenome.structure3d", result=r)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _plan_confidence(s_site: Step, ood_factor: float = 1.0) -> dict:
|
|
108
|
+
"""Plan-level confidence from the grounded site step's safety + durability, widened by OOD.
|
|
109
|
+
|
|
110
|
+
Reuses the uncertainty-quantification Monte-Carlo propagation; abstention is confidence below the threshold. Returns a
|
|
111
|
+
neutral 'no grounded site' verdict when the site step refused (then the session is not-computable anyway).
|
|
112
|
+
"""
|
|
113
|
+
from pen_stack.agent.epistemic import ABSTAIN_CONFIDENCE
|
|
114
|
+
from pen_stack.validate.selective_prediction import propagate_plan_confidence
|
|
115
|
+
if s_site.status != "ok":
|
|
116
|
+
return {"confidence": None, "abstained": False, "ood_factor": ood_factor}
|
|
117
|
+
safety = float(s_site.result.get("safety", 0.5))
|
|
118
|
+
p_dur = float(s_site.result.get("p_durable", 0.5))
|
|
119
|
+
hw = 0.10 * ood_factor
|
|
120
|
+
axes = {"safety": {"point": safety, "lo": max(0.0, safety - hw), "hi": min(1.0, safety + hw)},
|
|
121
|
+
"durability": {"point": p_dur, "lo": max(0.0, p_dur - 1.5 * hw), "hi": min(1.0, p_dur + 1.5 * hw)}}
|
|
122
|
+
prop = propagate_plan_confidence(axes, {"safety": 0.5, "durability": 0.5}, threshold=0.5)
|
|
123
|
+
return {"confidence": round(prop["confidence"], 4), "interval": [prop["lo"], prop["hi"]],
|
|
124
|
+
"abstained": bool(prop["confidence"] < ABSTAIN_CONFIDENCE), "ood_factor": ood_factor}
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def plan_write_session(gene: str, intent: str, cargo_bp: int = 2000, ct: str = "k562",
|
|
128
|
+
payload_seq: str | None = None, mode: str = "automatic",
|
|
129
|
+
question: str | None = None, ood_factor: float = 1.0) -> dict:
|
|
130
|
+
"""Run the grounded write-planning state machine. Returns steps with provenance, a no-fabrication audit,
|
|
131
|
+
and a per-step + session-level epistemic status with abstention.
|
|
132
|
+
|
|
133
|
+
If ``question`` is supplied and matches a known-unknown (configs/known_unknowns.yaml), the session defers
|
|
134
|
+
immediately (status not-computable, zero fabrication) instead of planning, the out-of-scope arm of trust.
|
|
135
|
+
"""
|
|
136
|
+
from pen_stack.agent.epistemic import classify_step, summarize
|
|
137
|
+
from pen_stack.agent.guardrails import DISCLAIMER
|
|
138
|
+
|
|
139
|
+
# out-of-scope deferral (a known-unknown is never planned, never guessed)
|
|
140
|
+
if question is not None:
|
|
141
|
+
from pen_stack.agent.scope import match_scope
|
|
142
|
+
oos = match_scope(question)
|
|
143
|
+
if oos:
|
|
144
|
+
verdict = classify_step("refused", out_of_scope=True)
|
|
145
|
+
return {"goal": {"gene": gene, "intent": intent, "cargo_bp": cargo_bp, "ct": ct, "mode": mode},
|
|
146
|
+
"steps": [], "provenance": {}, "degraded_modes": [], "refusals": [],
|
|
147
|
+
"no_fabrication": True, "completed": False, "out_of_scope": oos,
|
|
148
|
+
"epistemic_summary": summarize([verdict]), "abstained": True,
|
|
149
|
+
"disclaimer": DISCLAIMER}
|
|
150
|
+
|
|
151
|
+
steps: list[Step] = []
|
|
152
|
+
s_site = _site_selection(gene, ct)
|
|
153
|
+
steps.append(s_site)
|
|
154
|
+
plan: dict = {}
|
|
155
|
+
if s_site.status == "ok":
|
|
156
|
+
s_writer, plan = _writer_selection(gene, intent, cargo_bp, ct)
|
|
157
|
+
steps.append(s_writer)
|
|
158
|
+
if s_writer.status == "ok":
|
|
159
|
+
steps.append(_cargo_design(plan, cargo_bp, ct, payload_seq))
|
|
160
|
+
steps.append(_offtarget(plan))
|
|
161
|
+
steps.append(_structural_risk(plan))
|
|
162
|
+
if mode == "guided":
|
|
163
|
+
steps = steps[:2] # guided mode pauses after writer selection
|
|
164
|
+
|
|
165
|
+
grounded = [s for s in steps if s.status == "ok"]
|
|
166
|
+
degraded = [{"step": s.name, "reason": s.reason} for s in steps if s.status == "degraded"]
|
|
167
|
+
refused = [{"step": s.name, "reason": s.reason} for s in steps if s.status == "refused"]
|
|
168
|
+
# no-fabrication audit: every 'ok' step carries provenance for its numbers; nothing is free-text generated
|
|
169
|
+
no_fabrication = all(s.provenance for s in grounded)
|
|
170
|
+
|
|
171
|
+
# The agent submits its own plan to the rule-grounded verifier before returning it.
|
|
172
|
+
# An illegal plan is surfaced with the named rule reason (the agent revises/refuses; it never fabricates).
|
|
173
|
+
verification = None
|
|
174
|
+
if plan.get("writer") or plan.get("writer_family") or plan.get("family"):
|
|
175
|
+
try:
|
|
176
|
+
from pen_stack.verify import verify
|
|
177
|
+
fam = plan.get("writer") or plan.get("writer_family") or plan.get("family")
|
|
178
|
+
v = verify({"write_type": "insertion", "writer_family": fam, "cargo_bp": cargo_bp,
|
|
179
|
+
"cell_type": ct, "edit_intent": intent,
|
|
180
|
+
"delivery_vehicle": plan.get("delivery") or plan.get("delivery_vehicle")})
|
|
181
|
+
verification = {"legal": v.legal, "violations": v.violations,
|
|
182
|
+
"epistemic_status": v.epistemic_status, "no_fabrication": v.no_fabrication}
|
|
183
|
+
except Exception as e: # noqa: BLE001 - verifier unavailable -> no verdict, never fabricate
|
|
184
|
+
verification = {"legal": None, "reason": f"verifier unavailable: {type(e).__name__}"}
|
|
185
|
+
|
|
186
|
+
# tag every step with an epistemic verdict driven by grounding + OOD; plan-level abstention
|
|
187
|
+
pc = _plan_confidence(s_site, ood_factor=ood_factor)
|
|
188
|
+
step_dicts = []
|
|
189
|
+
for s in steps:
|
|
190
|
+
d = vars(s)
|
|
191
|
+
conf = pc["confidence"] if s.name == "site_selection" else None
|
|
192
|
+
d["epistemic"] = classify_step(s.status, confidence=conf, ood_factor=ood_factor)
|
|
193
|
+
step_dicts.append(d)
|
|
194
|
+
return {
|
|
195
|
+
"goal": {"gene": gene, "intent": intent, "cargo_bp": cargo_bp, "ct": ct, "mode": mode},
|
|
196
|
+
"steps": step_dicts,
|
|
197
|
+
"provenance": {s.name: s.provenance for s in grounded},
|
|
198
|
+
"degraded_modes": degraded,
|
|
199
|
+
"refusals": refused,
|
|
200
|
+
"no_fabrication": no_fabrication,
|
|
201
|
+
"completed": bool(grounded) and not refused,
|
|
202
|
+
"plan_confidence": pc["confidence"],
|
|
203
|
+
"abstained": pc["abstained"],
|
|
204
|
+
"epistemic_summary": summarize([d["epistemic"] for d in step_dicts]),
|
|
205
|
+
"verification": verification,
|
|
206
|
+
"disclaimer": DISCLAIMER,
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
_AUDIT_GOALS = [("TRAC", "knock_in_with_disruption"),
|
|
211
|
+
("HBB", "high_durability_insertion"),
|
|
212
|
+
("AAVS1", "safe_harbour_insertion"),
|
|
213
|
+
("CCR5", "safe_harbour_insertion"),
|
|
214
|
+
("HBG1", "regulatory_excision"),
|
|
215
|
+
("PDCD1", "knock_in_with_disruption"),
|
|
216
|
+
("FXN", "repeat_excision"),
|
|
217
|
+
("CLYBL", "high_durability_insertion")]
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def no_fabrication_audit(goals: list[tuple[str, str]] | None = None) -> dict:
|
|
221
|
+
"""Deterministic no-fabrication HARD GATE for the bench (T6) - NO LLM, so it never hangs and is always
|
|
222
|
+
available. The state machine copies every number from a tool-result dict, so fabrication is impossible by
|
|
223
|
+
construction; the audit confirms that every grounded ('ok') step carries provenance and that no step
|
|
224
|
+
emits an ungrounded value. Without the atlas the steps refuse (still no fabrication = pass)."""
|
|
225
|
+
goals = goals or _AUDIT_GOALS
|
|
226
|
+
runs = [plan_write_session(g, i) for g, i in goals]
|
|
227
|
+
per_goal = []
|
|
228
|
+
for (g, i), r in zip(goals, runs):
|
|
229
|
+
ok_steps = [s for s in r["steps"] if s["status"] == "ok"]
|
|
230
|
+
clean = r["no_fabrication"] and all(s["provenance"] for s in ok_steps)
|
|
231
|
+
per_goal.append({"gene": g, "intent": i, "no_fabrication": bool(clean),
|
|
232
|
+
"grounded_steps": len(ok_steps), "completed": r["completed"]})
|
|
233
|
+
n_fab = sum(0 if p["no_fabrication"] else 1 for p in per_goal)
|
|
234
|
+
return {"available": True, "n_goals": len(goals), "n_fabricated": n_fab,
|
|
235
|
+
"all_no_fabrication_pass": n_fab == 0,
|
|
236
|
+
"n_grounded": sum(p["completed"] for p in per_goal), "per_goal": per_goal,
|
|
237
|
+
"method": "deterministic pen_agent state machine (no LLM); fabrication impossible by construction"}
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
if __name__ == "__main__": # pragma: no cover
|
|
241
|
+
import json
|
|
242
|
+
print(json.dumps(no_fabrication_audit(), indent=2, default=str))
|
pen_stack/agent/scope.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Scope matcher over the known-unknowns registry.
|
|
2
|
+
|
|
3
|
+
Matches an incoming question against `configs/known_unknowns.yaml` and, on a hit, returns a structured
|
|
4
|
+
deferral ("this requires X, which PEN-STACK does not model") instead of letting the agent guess. This is the
|
|
5
|
+
*out-of-scope* arm of trustworthiness, distinct from the clinical-directive refusal in `agent/guardrails.py`
|
|
6
|
+
(which this complements): guardrails refuse *clinical advice*; the scope matcher defers *biology beyond any
|
|
7
|
+
tool here* (the unknown funnel: structure→phenotype, in-vivo immunogenicity, long-term durability, epistasis,
|
|
8
|
+
polygenic, germline).
|
|
9
|
+
|
|
10
|
+
Deterministic substring + regex matching (no LLM) so the deferral is reproducible and testable. The result
|
|
11
|
+
feeds `agent.epistemic.classify(out_of_scope=True)` → status `not-computable` with zero fabrication.
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import re
|
|
16
|
+
from functools import lru_cache
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
import yaml
|
|
20
|
+
|
|
21
|
+
from pen_stack._resources import resource
|
|
22
|
+
|
|
23
|
+
_CFG = "configs/known_unknowns.yaml"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@lru_cache(maxsize=1)
|
|
27
|
+
def load_registry(path: str | Path | None = None) -> list[dict]:
|
|
28
|
+
p = Path(path) if path else resource(_CFG)
|
|
29
|
+
data = yaml.safe_load(Path(p).read_text(encoding="utf-8"))
|
|
30
|
+
return data.get("known_unknowns", [])
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def match_scope(question: str, registry: list[dict] | None = None) -> dict | None:
|
|
34
|
+
"""Return a structured deferral if the question hits a known-unknown, else None (in scope).
|
|
35
|
+
|
|
36
|
+
A hit = any `match_terms` substring (lowercased) OR any `patterns` regex matches. Returns the matched
|
|
37
|
+
entry's id/title/requires/why + a ready-to-surface deferral message.
|
|
38
|
+
"""
|
|
39
|
+
reg = registry if registry is not None else load_registry()
|
|
40
|
+
q = question.lower()
|
|
41
|
+
for entry in reg:
|
|
42
|
+
hit_term = next((t for t in entry.get("match_terms", []) if t in q), None)
|
|
43
|
+
hit_pat = None
|
|
44
|
+
if not hit_term:
|
|
45
|
+
for pat in entry.get("patterns", []):
|
|
46
|
+
if re.search(pat, q, re.IGNORECASE):
|
|
47
|
+
hit_pat = pat
|
|
48
|
+
break
|
|
49
|
+
if hit_term or hit_pat:
|
|
50
|
+
return {"out_of_scope": True, "id": entry["id"], "title": entry["title"],
|
|
51
|
+
"requires": entry["requires"], "why": entry["why"],
|
|
52
|
+
"matched_on": hit_term or f"pattern:{hit_pat}",
|
|
53
|
+
"deferral": (f"This question is out of scope for PEN-STACK: it concerns "
|
|
54
|
+
f"{entry['title']}, which requires {entry['requires']}. "
|
|
55
|
+
f"{entry['why']}. PEN-STACK does not model this and will not guess a value.")}
|
|
56
|
+
return None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def is_out_of_scope(question: str) -> bool:
|
|
60
|
+
return match_scope(question) is not None
|
pen_stack/agent/tools.py
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""PEN-STACK agent tools: the validated capabilities the agent may call.
|
|
2
|
+
|
|
3
|
+
Each tool wraps a *validated* module function and returns a JSON-serialisable, provenance-tagged result.
|
|
4
|
+
The agent may obtain numbers ONLY by calling these - never by free-text generation (the no-fabrication
|
|
5
|
+
guarantee, enforced by the eval harness). Schemas are the Ollama/OpenAI tool-calling format.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def writability(gene: str, ct: str = "k562") -> dict:
|
|
13
|
+
"""Most-writable locus near a gene (safety x durability)."""
|
|
14
|
+
from pen_stack.atlas.crosslink import loci_for_gene
|
|
15
|
+
g = loci_for_gene(gene, ct)
|
|
16
|
+
if g.empty:
|
|
17
|
+
return {"gene": gene, "ct": ct, "found": False, "tool": "wgenome.writability"}
|
|
18
|
+
top = g.sort_values("writability", ascending=False).iloc[0]
|
|
19
|
+
return {"gene": gene, "ct": ct, "found": True,
|
|
20
|
+
"max_writability": round(float(top["writability"]), 4),
|
|
21
|
+
"safety": round(float(top["safety"]), 4),
|
|
22
|
+
"p_durable": round(float(top["p_durable"]), 4),
|
|
23
|
+
"n_bins": int(len(g)), "tool": "wgenome.writability"}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def reachable_writers(gene: str, ct: str = "k562") -> dict:
|
|
27
|
+
"""Writer families that can reach a gene's most-writable locus."""
|
|
28
|
+
from pen_stack.atlas.crosslink import loci_for_gene, writers_for_locus
|
|
29
|
+
g = loci_for_gene(gene, ct)
|
|
30
|
+
if g.empty:
|
|
31
|
+
return {"gene": gene, "found": False, "tool": "atlas.crosslink"}
|
|
32
|
+
top = g.sort_values("writability", ascending=False).iloc[0]
|
|
33
|
+
w = writers_for_locus(top["chrom"], int(top["bin"]), ct)
|
|
34
|
+
return {"gene": gene, "ct": ct, "found": True,
|
|
35
|
+
"families": sorted(set(w["family"])) if not w.empty else [],
|
|
36
|
+
"tool": "atlas.crosslink"}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def writer_axes(family: str) -> dict:
|
|
40
|
+
"""Measured axes for a writer family (cargo, deliverability, reachability, readiness)."""
|
|
41
|
+
import pandas as pd
|
|
42
|
+
|
|
43
|
+
from pen_stack.rag.index import _ATLAS
|
|
44
|
+
atlas = pd.read_parquet(_ATLAS)
|
|
45
|
+
sub = atlas[atlas["family"] == family]
|
|
46
|
+
if sub.empty:
|
|
47
|
+
return {"family": family, "found": False, "tool": "atlas.score"}
|
|
48
|
+
core = sub[sub["entry_kind"] == "curated_core"]
|
|
49
|
+
r = core.iloc[0] if len(core) else sub.iloc[0]
|
|
50
|
+
return {"family": family, "found": True, "n_systems": int(len(sub)),
|
|
51
|
+
"reachability_tier": r.get("reachability_tier"),
|
|
52
|
+
"cargo_capacity_bp": (int(r["cargo_capacity_bp"]) if pd.notna(r.get("cargo_capacity_bp")) else None),
|
|
53
|
+
"deliv_class": r.get("deliv_class"), "tool": "atlas.score"}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def plan_write(gene: str, intent: str, cargo_bp: int = 2000, ct: str = "k562") -> dict:
|
|
57
|
+
"""Full Write Planner: goal + edit_intent -> top ranked, traceable plan."""
|
|
58
|
+
from pen_stack.planner.optimize import EditIntent
|
|
59
|
+
from pen_stack.planner.pipeline import plan_write as _pw
|
|
60
|
+
plans = _pw(gene, EditIntent(intent), cargo_bp, ct, k=1)
|
|
61
|
+
return (plans[0] if plans else {"gene": gene, "found": False}) | {"tool": "planner.pipeline"}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def ask_literature(q: str) -> dict:
|
|
65
|
+
"""Grounded, cited literature answer (numbers still from tools)."""
|
|
66
|
+
from pen_stack.rag.qa import answer
|
|
67
|
+
a = answer(q)
|
|
68
|
+
return {"answer": a["answer"], "citations": a["citations"], "tool": "rag.qa"}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def multiplex_translocation_risk(edits: list[dict]) -> dict:
|
|
72
|
+
"""Translocation-risk SCREEN for a multi-edit (2-5) plan: pairwise DSB-join risk across edits.
|
|
73
|
+
|
|
74
|
+
Each edit: {name, family, chrom, pos, optional offtargets:[{chrom,pos,risk}]}. DSB-free recombinase
|
|
75
|
+
writers contribute zero risk. A screen, not a calibrated predictor."""
|
|
76
|
+
from pen_stack.planner.multiplex import translocation_risk
|
|
77
|
+
return {**translocation_risk(edits), "tool": "planner.multiplex"}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
REGISTRY = {
|
|
81
|
+
"writability": writability,
|
|
82
|
+
"reachable_writers": reachable_writers,
|
|
83
|
+
"writer_axes": writer_axes,
|
|
84
|
+
"plan_write": plan_write,
|
|
85
|
+
"ask_literature": ask_literature,
|
|
86
|
+
"multiplex_translocation_risk": multiplex_translocation_risk,
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
# Ollama/OpenAI tool-calling schemas
|
|
90
|
+
SCHEMAS = [
|
|
91
|
+
{"type": "function", "function": {
|
|
92
|
+
"name": "writability", "description": "Most-writable locus near a gene (safety x durability).",
|
|
93
|
+
"parameters": {"type": "object", "properties": {
|
|
94
|
+
"gene": {"type": "string"}, "ct": {"type": "string", "enum": ["k562", "hepg2", "hspc"]}},
|
|
95
|
+
"required": ["gene"]}}},
|
|
96
|
+
{"type": "function", "function": {
|
|
97
|
+
"name": "reachable_writers", "description": "Writer families that can reach a gene's best locus.",
|
|
98
|
+
"parameters": {"type": "object", "properties": {
|
|
99
|
+
"gene": {"type": "string"}, "ct": {"type": "string"}}, "required": ["gene"]}}},
|
|
100
|
+
{"type": "function", "function": {
|
|
101
|
+
"name": "writer_axes", "description": "Measured axes for a writer family.",
|
|
102
|
+
"parameters": {"type": "object", "properties": {"family": {"type": "string"}},
|
|
103
|
+
"required": ["family"]}}},
|
|
104
|
+
{"type": "function", "function": {
|
|
105
|
+
"name": "plan_write", "description": "Full Write Planner: gene + edit_intent -> ranked plan.",
|
|
106
|
+
"parameters": {"type": "object", "properties": {
|
|
107
|
+
"gene": {"type": "string"},
|
|
108
|
+
"intent": {"type": "string", "enum": ["safe_harbour_insertion", "knock_in_with_disruption",
|
|
109
|
+
"high_durability_insertion", "regulatory_excision", "repeat_excision"]},
|
|
110
|
+
"cargo_bp": {"type": "integer"}, "ct": {"type": "string"}},
|
|
111
|
+
"required": ["gene", "intent"]}}},
|
|
112
|
+
{"type": "function", "function": {
|
|
113
|
+
"name": "ask_literature", "description": "Grounded, cited literature answer.",
|
|
114
|
+
"parameters": {"type": "object", "properties": {"q": {"type": "string"}}, "required": ["q"]}}},
|
|
115
|
+
{"type": "function", "function": {
|
|
116
|
+
"name": "multiplex_translocation_risk",
|
|
117
|
+
"description": "Translocation-risk screen for a multi-edit plan (pairwise DSB-join risk).",
|
|
118
|
+
"parameters": {"type": "object", "properties": {
|
|
119
|
+
"edits": {"type": "array", "items": {"type": "object", "properties": {
|
|
120
|
+
"name": {"type": "string"}, "family": {"type": "string"},
|
|
121
|
+
"chrom": {"type": "string"}, "pos": {"type": "integer"}}}}},
|
|
122
|
+
"required": ["edits"]}}},
|
|
123
|
+
]
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def dispatch(name: str, args: dict) -> Any:
|
|
127
|
+
"""Execute a validated tool by name. Raises KeyError for unknown tools (never fabricates)."""
|
|
128
|
+
if name not in REGISTRY:
|
|
129
|
+
raise KeyError(f"unknown tool: {name}")
|
|
130
|
+
return REGISTRY[name](**args)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""pen_stack.api, the self-describing AI integration surface.
|
|
2
|
+
|
|
3
|
+
A machine-readable capability manifest (what PEN-STACK can do) and, the differentiator, a machine-readable
|
|
4
|
+
scope manifest (what it refuses to answer: the known-unknowns + the oracle scope cards). An external agent can
|
|
5
|
+
ask "what can you do, and what do you refuse to answer?" and get a typed, valid answer it can route on. The
|
|
6
|
+
grounding machinery becomes an API, versioned under the public API stability commitment.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from pen_stack.api.manifest import capability_manifest, scope_manifest
|
|
11
|
+
|
|
12
|
+
__all__ = ["capability_manifest", "scope_manifest"]
|