syntheogenesis / tests /test_agent_tools.py
Tengo Gzirishvili
Fix DE tool "forgetting" a just-fetched sequence — it needed protein, got DNA
6758027
Raw
History Blame Contribute Delete
23.1 kB
"""Tests for dee/core/agent_tools.py — the tools the Turing agent can call.
Boundary: these tests exercise agent_tools.py's own argument validation and
result shaping, not the underlying dee.core.crispr / dee.core.primers /
dee.core.scoring engines (those have their own test suites) — so
find_guides/design_primers/scoring/evolve are monkeypatched rather than
run for real.
"""
from types import SimpleNamespace
import pandas as pd
import pytest
from dee.core import agent_tools, scoring
# ─── execute_tool dispatch ──────────────────────────────────────────────
def test_execute_tool_unknown_name_returns_error():
result = agent_tools.execute_tool("not_a_real_tool", {}, False)
assert result["ok"] is False
assert "unknown tool" in result["error"]
@pytest.mark.parametrize("name", ["fetch_sequence", "design_crispr_guides", "design_primers"])
def test_execute_tool_requires_signin_for_anonymous(name):
result = agent_tools.execute_tool(name, {}, True)
assert result == {
"ok": False,
"kind": "signin_required",
"error": "This requires a free account. Sign in to continue.",
}
def test_execute_tool_never_raises_on_unexpected_tool_exception(monkeypatch):
def boom(args):
raise RuntimeError("unexpected")
monkeypatch.setitem(agent_tools._TOOLS, "design_crispr_guides",
{"fn": boom, "requires_signin": False})
result = agent_tools.execute_tool("design_crispr_guides", {}, False)
assert result["ok"] is False
assert "failed" in result["error"]
# ─── fetch_sequence ──────────────────────────────────────────────────────
# Added 2026-07-11 — chat previously had no way to resolve a gene name /
# accession to a sequence, so a request like "find CRISPR guides in human
# GFP" was a dead end even though dee.core.resolve.resolve_target() (the
# same "paste anything" resolver behind POST /api/crispr/resolve) already
# did exactly this.
def test_fetch_sequence_tool_missing_text_errors():
result = agent_tools.execute_tool("fetch_sequence", {}, False)
assert result == {"ok": False, "error": "missing 'text'"}
def test_fetch_sequence_tool_rejects_oversized_text():
result = agent_tools.execute_tool(
"fetch_sequence", {"text": "A" * 1_000_001}, False,
)
assert result["ok"] is False
assert "1 Mbp" in result["error"]
def test_fetch_sequence_tool_shapes_successful_result(monkeypatch):
seen = {}
def fake_resolve_target(text, organism=""):
seen.update(text=text, organism=organism)
return {"ok": True, "kind": "gene", "sequence": "ATGAAA",
"gene_symbol": "GFP", "label": "GFP · ENST00000 · CDS 6 nt",
"source": "ensembl"}
monkeypatch.setattr("dee.core.resolve.resolve_target", fake_resolve_target)
result = agent_tools.execute_tool(
"fetch_sequence", {"text": "GFP", "organism": "human"}, False,
)
assert seen == {"text": "GFP", "organism": "human"}
assert result == {
"ok": True, "kind": "gene", "sequence": "ATGAAA", "length": 6,
"gene_symbol": "GFP", "label": "GFP · ENST00000 · CDS 6 nt",
"source": "ensembl",
}
def test_fetch_sequence_tool_invalid_organism_falls_back_to_empty(monkeypatch):
seen = {}
monkeypatch.setattr("dee.core.resolve.resolve_target",
lambda text, organism="": seen.update(organism=organism) or {"ok": True, "sequence": "ATG"})
agent_tools.execute_tool("fetch_sequence", {"text": "GFP", "organism": "mars"}, False)
assert seen["organism"] == ""
def test_fetch_sequence_tool_passes_through_resolve_failure(monkeypatch):
monkeypatch.setattr(
"dee.core.resolve.resolve_target",
lambda text, organism="": {"ok": False, "error": "Couldn't find “XYZ” in human."},
)
result = agent_tools.execute_tool("fetch_sequence", {"text": "XYZ", "organism": "human"}, False)
assert result == {"ok": False, "error": "Couldn't find “XYZ” in human."}
def test_fetch_sequence_tool_engine_exception_becomes_error_result(monkeypatch):
def boom(text, organism=""):
raise RuntimeError("Ensembl is down")
monkeypatch.setattr("dee.core.resolve.resolve_target", boom)
result = agent_tools.execute_tool("fetch_sequence", {"text": "GFP", "organism": "human"}, False)
assert result["ok"] is False
assert "Ensembl is down" not in result["error"]
# ─── design_crispr_guides ───────────────────────────────────────────────
def _fake_guide(**overrides):
base = dict(rank=1, strand="+", position=10, spacer="ACGTACGTACGTACGTACGT",
pam="AGG", composite_score=0.751, on_target_score=0.801,
gc_pct=50.05, notes="clean")
base.update(overrides)
return SimpleNamespace(**base)
def test_crispr_tool_missing_sequence_errors():
result = agent_tools.execute_tool("design_crispr_guides", {}, False)
assert result == {"ok": False, "error": "missing 'sequence'"}
def test_crispr_tool_sequence_too_long_errors():
result = agent_tools.execute_tool(
"design_crispr_guides", {"sequence": "A" * 1_000_001}, False,
)
assert result["ok"] is False
assert "1 Mbp" in result["error"]
def test_crispr_tool_shapes_and_bounds_args(monkeypatch):
seen = {}
def fake_find_guides(sequence, *, enzyme, max_results, mode):
seen.update(sequence=sequence, enzyme=enzyme, max_results=max_results, mode=mode)
return [_fake_guide()]
monkeypatch.setattr("dee.core.crispr.find_guides", fake_find_guides)
result = agent_tools.execute_tool("design_crispr_guides", {
"sequence": "ACGT", "enzyme": "WEIRD", "mode": "also weird", "max_results": 9999,
}, False)
# Invalid enzyme/mode fall back to the safe defaults rather than erroring
# (mirrors POST /api/crispr/design's own tolerant handling).
assert seen["enzyme"] == "cas9"
assert seen["mode"] == "knockout"
# Bounded to the chat-sized cap (50), not the REST UI's 500.
assert seen["max_results"] == 50
assert result["ok"] is True
assert result["n_guides"] == 1
guide = result["guides"][0]
assert guide["spacer"] == "ACGTACGTACGTACGTACGT"
assert guide["composite_score"] == 0.751
assert guide["gc_pct"] == round(50.05, 1) # rounded to 1 dp
def test_crispr_tool_value_error_from_engine_becomes_error_result(monkeypatch):
def fake_find_guides(sequence, *, enzyme, max_results, mode):
raise ValueError("sequence too short for any PAM site")
monkeypatch.setattr("dee.core.crispr.find_guides", fake_find_guides)
result = agent_tools.execute_tool("design_crispr_guides", {"sequence": "AC"}, False)
assert result == {"ok": False, "error": "sequence too short for any PAM site"}
def test_crispr_tool_applies_field_prior_when_populated(monkeypatch):
# Before 2026-07-11 this tool never consulted the cross-user aggregate at
# all — it went straight from find_guides to the response, unlike
# POST /api/crispr/design which always calibrates. Verifies the chat
# tool now calls the same calibrate_crispr_guides() and reports how many
# substitution keys were actually blended in.
monkeypatch.setattr("dee.core.crispr.find_guides", lambda *a, **kw: [_fake_guide()])
from dee.core import outcomes as O
fake_prior = O.Prior(tool="crispr", effects={"k1": 0.1, "k2": -0.2}, n_users={}, n_obs={}, weight=0.2)
monkeypatch.setattr("dee.core.outcomes.load_cached_tool_prior", lambda tool: fake_prior)
calibrated_with = {}
def fake_calibrate(guides, prior):
calibrated_with["guides"] = guides
calibrated_with["prior"] = prior
return guides
monkeypatch.setattr("dee.core.outcomes.calibrate_crispr_guides", fake_calibrate)
result = agent_tools.execute_tool("design_crispr_guides", {"sequence": "ACGT"}, False)
assert result["ok"] is True
assert result["field_prior_keys"] == 2
assert calibrated_with["prior"] is fake_prior
def test_crispr_tool_field_prior_absent_is_a_noop(monkeypatch):
# No monkeypatch of load_cached_tool_prior — exercises the real (empty,
# since no SUPABASE_* env vars in tests) path, confirming the default is
# silent and harmless rather than an error.
monkeypatch.setattr("dee.core.crispr.find_guides", lambda *a, **kw: [_fake_guide()])
result = agent_tools.execute_tool("design_crispr_guides", {"sequence": "ACGT"}, False)
assert result["ok"] is True
assert result["field_prior_keys"] == 0
# ─── design_primers ──────────────────────────────────────────────────────
def test_primers_tool_missing_template_errors():
result = agent_tools.execute_tool("design_primers", {}, False)
assert result["ok"] is False
assert "template" in result["error"].lower()
def test_primers_tool_rejects_non_dna_chars():
result = agent_tools.execute_tool("design_primers", {"template": "ACGTXYZ"}, False)
assert result["ok"] is False
assert "A/C/G/T/N" in result["error"]
def test_primers_tool_rejects_oversized_template():
result = agent_tools.execute_tool("design_primers", {"template": "A" * 70_000}, False)
assert result["ok"] is False
assert "bp" in result["error"]
def test_primers_tool_shapes_successful_result(monkeypatch):
def fake_design_primers(template, target_start, target_end):
return {
"ok": True,
"pairs": [{
"forward": {"seq": "ACGTACGTACGTACGT", "tm": 59.5, "gc": 50.0, "len": 16},
"reverse": {"seq": "TTTTAAAACCCCGGGG", "tm": 60.1, "gc": 50.0, "len": 16},
"product_size": 250,
"tm_diff": 0.6,
"cross_dimer": 1,
"score": 0.9,
}],
"n_forward": 4, "n_reverse": 3, "warnings": [],
}
monkeypatch.setattr("dee.core.primers.design_primers", fake_design_primers)
result = agent_tools.execute_tool("design_primers", {"template": "ACGT" * 100}, False)
assert result["ok"] is True
assert result["n_forward"] == 4
assert result["pairs"] == [{
"forward_seq": "ACGTACGTACGTACGT", "forward_tm": 59.5,
"reverse_seq": "TTTTAAAACCCCGGGG", "reverse_tm": 60.1,
"product_size": 250,
}]
# Internal candidate metadata (hairpin/self-dimer/score) is dropped —
# not useful read inline in chat.
assert "score" not in result["pairs"][0]
def test_primers_tool_passes_through_engine_validation_failure(monkeypatch):
monkeypatch.setattr("dee.core.primers.design_primers",
lambda template, target_start, target_end: {"ok": False, "error": "no pair found"})
result = agent_tools.execute_tool("design_primers", {"template": "ACGT" * 100}, False)
assert result == {"ok": False, "error": "no pair found"}
# ─── design_variant_library ──────────────────────────────────────────────
_TEST_PROTEIN = "MSKGEELFTGVVPILVELDGDVNGHKFSVSGEGEGDATYGKLTLKFICTTG"
def _fake_scores_df(n=8):
return pd.DataFrame({
"position": list(range(n)), "wt_aa": ["A"] * n, "mut_aa": ["G"] * n,
"delta_ll": [float(i) for i in range(n)],
})
def _fake_variant(**overrides):
base = dict(rank=1, mutation_labels=("A1G", "A2G"), fitness=1.2345)
base.update(overrides)
return SimpleNamespace(**base)
def test_variant_library_tool_missing_sequence_errors():
result = agent_tools.execute_tool("design_variant_library", {}, False)
assert result == {"ok": False, "error": "missing 'sequence'"}
def test_variant_library_tool_rejects_non_protein_chars():
result = agent_tools.execute_tool("design_variant_library", {"sequence": "ACGT123"}, False)
assert result["ok"] is False
assert "protein" in result["error"].lower()
def test_variant_library_tool_rejects_oversized_sequence():
result = agent_tools.execute_tool(
"design_variant_library", {"sequence": "A" * (agent_tools._MAX_PROTEIN_AA + 1)}, False,
)
assert result["ok"] is False
assert str(agent_tools._MAX_PROTEIN_AA) in result["error"]
def test_variant_library_tool_auto_translates_dna_input(monkeypatch):
# fetch_sequence (and CRISPR/primer tools) all deal in DNA — a model
# chaining "fetch this gene, then DE it" naturally ends up holding DNA,
# which used to fail this tool's plain protein-alphabet check with no
# path forward (found 2026-07-12, looked to the user like the model
# had forgotten the sequence it had just fetched). ATG + 8x GCC (Ala) +
# TAA stop = a real, minimal CDS.
dna = "ATG" + "GCC" * 8 + "TAA"
monkeypatch.setattr("dee.core.scoring.get_scorer", lambda model: "the-scorer")
def fake_score_guarded(scorer, protein, *, wait_timeout=None):
assert protein == "MAAAAAAAA" # the DNA translated, not passed through raw
return _fake_scores_df(n=len(protein))
monkeypatch.setattr("dee.core.scoring.score_guarded", fake_score_guarded)
monkeypatch.setattr("dee.models.scorer.top_percentile_pool", lambda df, percentile: df)
monkeypatch.setattr("dee.optimizer.search.evolve", lambda pool, cfg: [_fake_variant(rank=1)])
result = agent_tools.execute_tool("design_variant_library", {"sequence": dna}, False)
assert result["ok"] is True
assert result["n_scored_positions"] == 9
def test_variant_library_tool_dna_translate_failure_falls_back_to_protein_check(monkeypatch):
# A string that LOOKS like DNA (all A/C/G/T, length divisible by 3) but
# isn't a valid CDS (no ATG start, so translate_dna's own validation
# rejects it) must not hard-fail — it falls through to treating the
# raw text as protein instead. "CCC"*10 is also a valid (if unusual)
# all-proline protein under the plain amino-acid alphabet check, so it
# proceeds rather than surfacing a confusing "translation failed" error
# for something that was never meant to be DNA in the first place.
not_a_cds = "CCC" * 10 # divisible by 3, all-ACGT, but no ATG start
monkeypatch.setattr("dee.core.scoring.get_scorer", lambda model: "the-scorer")
def fake_score_guarded(scorer, protein, *, wait_timeout=None):
assert protein == not_a_cds # NOT translated — used as-is
return _fake_scores_df(n=len(protein))
monkeypatch.setattr("dee.core.scoring.score_guarded", fake_score_guarded)
monkeypatch.setattr("dee.models.scorer.top_percentile_pool", lambda df, percentile: df)
monkeypatch.setattr("dee.optimizer.search.evolve", lambda pool, cfg: [_fake_variant(rank=1)])
result = agent_tools.execute_tool("design_variant_library", {"sequence": not_a_cds}, False)
assert result["ok"] is True
def test_variant_library_tool_shapes_and_bounds_args(monkeypatch):
seen = {}
monkeypatch.setattr("dee.core.scoring.get_scorer", lambda model: "the-scorer")
def fake_score_guarded(scorer, protein, *, wait_timeout=None):
seen.update(scorer=scorer, protein=protein, wait_timeout=wait_timeout)
return _fake_scores_df()
monkeypatch.setattr("dee.core.scoring.score_guarded", fake_score_guarded)
monkeypatch.setattr("dee.models.scorer.top_percentile_pool",
lambda df, percentile: df)
def fake_evolve(pool, cfg):
seen["k"] = cfg.k
seen["max_mutations"] = cfg.max_mutations
seen["min_mutations"] = cfg.min_mutations
return [_fake_variant(rank=1), _fake_variant(rank=2, mutation_labels=("A3G",), fitness=0.9)]
monkeypatch.setattr("dee.optimizer.search.evolve", fake_evolve)
result = agent_tools.execute_tool("design_variant_library", {
"sequence": _TEST_PROTEIN, "host": "WEIRD", "k": 9999, "max_mutations": 99,
}, False)
assert seen["protein"] == _TEST_PROTEIN
assert seen["wait_timeout"] == 20.0
# Invalid host falls back to the safe default rather than erroring.
assert seen["k"] == 20 # clamped to the chat cap, not the REST 200
assert seen["max_mutations"] == 6
assert seen["min_mutations"] <= seen["max_mutations"]
assert result["ok"] is True
assert result["host"] == "e_coli"
assert result["n_scored_positions"] == 8
assert result["variants"] == [
{"rank": 1, "mutations": ["A1G", "A2G"], "fitness": round(1.2345, 3)},
{"rank": 2, "mutations": ["A3G"], "fitness": 0.9},
]
def test_variant_library_tool_returns_real_dna_per_variant(monkeypatch):
# Before 2026-07-12 this tool stopped at mutation labels ("W44K") with
# no actual sequence — nothing to copy/paste or order. Uses REAL
# search.Mutation/Variant (not the loose SimpleNamespace _fake_variant
# above, which lacks .mutations and would silently fail inside
# variants_to_dataframe's apply_variant() — a gap that let the earlier,
# broken version of this change pass its own tests without actually
# exercising the encoding path).
from dee.optimizer.search import Mutation, Variant
monkeypatch.setattr("dee.core.scoring.get_scorer", lambda model: "the-scorer")
monkeypatch.setattr("dee.core.scoring.score_guarded",
lambda scorer, protein, **kw: _fake_scores_df())
monkeypatch.setattr("dee.models.scorer.top_percentile_pool", lambda df, percentile: df)
# _TEST_PROTEIN[0] == "M", _TEST_PROTEIN[1] == "S" — real WT residues,
# so apply_variant()'s own mismatch check passes.
real_variant = Variant(
mutations=(Mutation(position=0, wt_aa="M", mut_aa="A", delta_ll=1.0),
Mutation(position=1, wt_aa="S", mut_aa="G", delta_ll=0.5)),
fitness=1.5, rank=1,
)
monkeypatch.setattr("dee.optimizer.search.evolve", lambda pool, cfg: [real_variant])
result = agent_tools.execute_tool(
"design_variant_library", {"sequence": _TEST_PROTEIN, "host": "e_coli"}, False,
)
assert result["ok"] is True
variant = result["variants"][0]
assert variant["mutations"] == ["M1A", "S2G"]
assert variant["protein"][:2] == "AG" # M1A, S2G applied
assert variant["protein"][2:] == _TEST_PROTEIN[2:] # rest unchanged
assert isinstance(variant["dna"], str) and len(variant["dna"]) > 0
assert variant["dna"].strip("ACGT") == "" # real, plain DNA — no placeholder text
# AA positions, not an nt-level diff against WT — see the long comment
# at this field's definition in agent_tools.py for why.
assert variant["mutated_positions"] == [0, 1]
assert variant["length_bp"] == len(variant["dna"])
def test_variant_library_tool_busy_returns_kind_busy(monkeypatch):
monkeypatch.setattr("dee.core.scoring.get_scorer", lambda model: "the-scorer")
def fake_score_guarded(scorer, protein, *, wait_timeout=None):
raise scoring.ScoringBusyError("busy")
monkeypatch.setattr("dee.core.scoring.score_guarded", fake_score_guarded)
result = agent_tools.execute_tool("design_variant_library", {"sequence": _TEST_PROTEIN}, False)
assert result["ok"] is False
assert result["kind"] == "busy"
def test_variant_library_tool_scoring_exception_becomes_error_result(monkeypatch):
monkeypatch.setattr("dee.core.scoring.get_scorer", lambda model: "the-scorer")
def fake_score_guarded(scorer, protein, *, wait_timeout=None):
raise RuntimeError("torch blew up")
monkeypatch.setattr("dee.core.scoring.score_guarded", fake_score_guarded)
result = agent_tools.execute_tool("design_variant_library", {"sequence": _TEST_PROTEIN}, False)
assert result["ok"] is False
assert "torch blew up" not in result["error"]
def test_variant_library_tool_applies_field_prior_when_populated(monkeypatch):
# Before 2026-07-11 this tool went straight from scoring to search — it
# never consulted the cross-user aggregate at all, unlike POST /api/run
# which always blends it into round-1 scores. Verifies the chat tool now
# does the same blend and reports how many substitution types applied.
monkeypatch.setattr("dee.core.scoring.get_scorer", lambda model: "the-scorer")
monkeypatch.setattr("dee.core.scoring.score_guarded",
lambda scorer, protein, **kw: _fake_scores_df())
monkeypatch.setattr("dee.models.scorer.top_percentile_pool", lambda df, percentile: df)
from dee.core import aggregate as agg
fake_prior = agg.GlobalPrior(effects={("A", "G"): 2.0}, n_users={}, n_obs={})
monkeypatch.setattr("dee.core.aggregate.load_cached_global_prior", lambda: fake_prior)
seen = {}
def fake_evolve(pool, cfg):
seen["delta_ll"] = list(pool["delta_ll"])
return [_fake_variant(rank=1)]
monkeypatch.setattr("dee.optimizer.search.evolve", fake_evolve)
result = agent_tools.execute_tool("design_variant_library", {"sequence": _TEST_PROTEIN}, False)
assert result["ok"] is True
assert result["field_prior_substitutions"] == 1
# Every row is (A -> G) in _fake_scores_df, weight 0.3 * effect 2.0 = +0.6
# on top of the original delta_ll values (0..7).
assert seen["delta_ll"] == [float(i) + 0.6 for i in range(8)]
def test_variant_library_tool_field_prior_absent_is_a_noop(monkeypatch):
# No monkeypatch of load_cached_global_prior — exercises the real
# (empty, since no SUPABASE_* env vars in tests) path, confirming the
# default is silent and harmless rather than an error.
monkeypatch.setattr("dee.core.scoring.get_scorer", lambda model: "the-scorer")
monkeypatch.setattr("dee.core.scoring.score_guarded",
lambda scorer, protein, **kw: _fake_scores_df())
monkeypatch.setattr("dee.models.scorer.top_percentile_pool", lambda df, percentile: df)
monkeypatch.setattr("dee.optimizer.search.evolve", lambda pool, cfg: [_fake_variant(rank=1)])
result = agent_tools.execute_tool("design_variant_library", {"sequence": _TEST_PROTEIN}, False)
assert result["ok"] is True
assert result["field_prior_substitutions"] == 0
def test_variant_library_tool_evolve_value_error_becomes_error_result(monkeypatch):
monkeypatch.setattr("dee.core.scoring.get_scorer", lambda model: "the-scorer")
monkeypatch.setattr("dee.core.scoring.score_guarded",
lambda scorer, protein, **kw: _fake_scores_df())
monkeypatch.setattr("dee.models.scorer.top_percentile_pool", lambda df, percentile: df)
def fake_evolve(pool, cfg):
raise ValueError("Filtered mutation pool is empty")
monkeypatch.setattr("dee.optimizer.search.evolve", fake_evolve)
result = agent_tools.execute_tool("design_variant_library", {"sequence": _TEST_PROTEIN}, False)
assert result == {"ok": False, "error": "Filtered mutation pool is empty"}