Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions chorus/core/interval.py
Original file line number Diff line number Diff line change
Expand Up @@ -667,10 +667,15 @@ def _alig2cigar(alignment: Alignment) -> list[CigarEntry]:
c = CigarInsertion(seq=query[querypos:querypos+ilength(l)])
elif g == CigarDeletion.cigar_symbol:
c = CigarDeletion(length=ilength(l))
elif g == CigarNotEqual.cigar_symbol:
elif g == CigarNotEqual.cigar_symbol:
c = CigarNotEqual(seq=query[querypos:querypos+ilength(l)])
else:
raise Exception('')
# Was a bare Exception('') — meaningless if a user ever
# hit it. Known CIGAR symbols are ``= M I D X``; anything
# else is a parser error. v26 P1 #13.
raise IntervalException(
f"Unknown CIGAR symbol {g!r} (expected one of '=MIDX')"
)
if c.consumes_query:
querypos += len(c)
cigar_groups.append(c)
Expand Down
11 changes: 10 additions & 1 deletion chorus/mcp/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,20 +128,29 @@ def _safe_tool(fn):

Wraps the function body only; does not interfere with FastMCP's
registration (apply *inside* ``@mcp.tool()``).

Set ``CHORUS_MCP_DEBUG=1`` to include ``"traceback": ...`` in the
returned dict — useful when debugging a tool call through an MCP
client that only prints the JSON reply (v26 P1 #18).
"""
import functools
import os
import traceback as _traceback

@functools.wraps(fn)
def wrapper(*args, **kwargs):
try:
return fn(*args, **kwargs)
except Exception as exc:
logger.exception("MCP tool %s failed", fn.__name__)
return {
payload = {
"error": str(exc) or type(exc).__name__,
"error_type": type(exc).__name__,
"tool": fn.__name__,
}
if os.environ.get("CHORUS_MCP_DEBUG", "").lower() in ("1", "true", "yes"):
payload["traceback"] = _traceback.format_exc()
return payload

return wrapper

Expand Down
9 changes: 8 additions & 1 deletion chorus/oracles/borzoi_source/templates/predict_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,14 @@
meta = get_metadata()
track_indices = meta.id2index(args['assay_ids'])
if any(map(lambda x: x is None, track_indices)):
raise Exception(f"Some assay IDs not found in metadata: {args['assay_ids']}")
missing = [a for a, i in zip(args['assay_ids'], track_indices) if i is None]
# Use ValueError (not bare Exception) so the env-runner surfaces a
# recognisable error type. v26 P1 #12.
raise ValueError(
f"Borzoi track identifier(s) not found in metadata: {missing}. "
f"Use oracle.get_track_info() to list valid identifiers, or "
f"oracle.get_track_info(pattern) to search (e.g. 'DNASE:K562')."
)
# Extract predictions for selected tracks
selected_predictions = pred[:, track_indices]
result = selected_predictions.tolist()
9 changes: 8 additions & 1 deletion chorus/oracles/enformer_source/templates/predict_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,14 @@
meta = get_metadata()
track_indices = meta.id2index(args['assay_ids'])
if any(map(lambda x: x is None, track_indices)):
raise Exception(f"Some assay IDs not found in metadata: {args['assay_ids']}")
missing = [a for a, i in zip(args['assay_ids'], track_indices) if i is None]
# Use ValueError (not bare Exception) so the env-runner surfaces a
# recognisable error type through its subprocess wrapper. v26 P1 #12.
raise ValueError(
f"Enformer track identifier(s) not found in metadata: {missing}. "
f"Use oracle.get_track_info() to list valid identifiers, or "
f"oracle.get_track_info(pattern) to search (e.g. 'DNASE:K562')."
)
# Extract predictions for selected tracks
selected_predictions = human_predictions[:, track_indices]
result = selected_predictions.tolist()
46 changes: 46 additions & 0 deletions tests/test_prediction_methods.py
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,52 @@ def test_chorus_device_env_var_forces_cpu(self, monkeypatch):
oracle = MockOracle(reference_fasta=str(self.fasta_path))
assert oracle.device is None

def test_unknown_track_id_gives_actionable_error(self):
"""Invalid assay_id must raise InvalidAssayError with a pointer
to list_tracks / get_track_info — not silently substitute track 0
(previous behaviour corrupted predictions) or surface a cryptic
TypeError('NoneType' is not subscriptable).

Regression for v26 P0 finding (fix in 96fc28d). Exercises the
Enformer direct-load code path via a stubbed metadata object.
"""
import types
from chorus.core.exceptions import InvalidAssayError

try:
from chorus.oracles import enformer as enformer_mod
except ImportError:
pytest.skip("enformer module not importable")

# Fake metadata that misses every lookup.
fake_meta = types.SimpleNamespace(
get_track_by_identifier=lambda x: None,
get_tracks_by_description=lambda x: [],
)
stub = type("Stub", (), {})()

from chorus.oracles.enformer_source import enformer_metadata as meta_mod
orig = meta_mod.get_metadata
meta_mod.get_metadata = lambda: fake_meta
try:
# _validate_assay_ids is the user-facing gate called by
# OracleBase.predict() before _get_assay_indices. Both bad
# ENCFF IDs and bad descriptions must raise before reaching
# the model.
with pytest.raises(InvalidAssayError, match="(list_tracks|search_tracks)"):
enformer_mod.EnformerOracle._validate_assay_ids(
stub, ["ENCFF999BADID"]
)
with pytest.raises(InvalidAssayError, match="(list_tracks|search_tracks)"):
enformer_mod.EnformerOracle._validate_assay_ids(
stub, ["totally made up track name"]
)
# Empty input must not raise.
enformer_mod.EnformerOracle._validate_assay_ids(stub, None)
enformer_mod.EnformerOracle._validate_assay_ids(stub, [])
finally:
meta_mod.get_metadata = orig

def test_error_handling_model_not_loaded(self):
"""Test error when model not loaded."""
unloaded_oracle = MockOracle(reference_fasta=str(self.fasta_path))
Expand Down
Loading