Skip to content

Commit 55666c5

Browse files
committed
fix: resolve all code review issues (Critical+Important+Minor)
- fix(variants): TypeError on list.get() → list[10] with bounds check - fix(pyproject): build-backend setuptools.backends → setuptools.build_meta - fix(pyproject): move pysam/pyvcf3 to optional [bio], remove unused langchain-core - fix(pyproject): correct GitHub URLs to ImL1s/dogneo - fix(cli/rank): implement actual peptide generation pipeline instead of placeholder - fix(cli/report): wire up ReportGenerator with pre_rendered_candidates support - fix(ranking): agretopicity now uses _score_agretopicity() when WT binding available - feat(report): add pre_rendered_candidates param to generate_html/generate_markdown - feat(ranking): add wt_binding_nm field to NeoantigenCandidate dataclass
1 parent 555183c commit 55666c5

5 files changed

Lines changed: 113 additions & 29 deletions

File tree

dogneo/cli.py

Lines changed: 68 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -130,15 +130,42 @@ def rank(
130130
# Step 3: Generate peptides
131131
click.echo("🔬 Generating mutant peptides...")
132132
mhci_lens = [int(x) for x in mhci_lengths.split(",")]
133-
# (Simplified — would use protein DB in full pipeline)
134133
click.echo(f" Peptide lengths: MHC-I {mhci_lens}")
135134

136-
# Step 4: Ranking (placeholder candidates for direct VCF mode)
135+
# NOTE: full peptide generation requires a canine protein FASTA database.
136+
# In the full pipeline (Snakemake), this is handled by the alignment steps.
137+
from dogneo.core.peptides import ProteinDatabase, generate_peptides
138+
from dogneo.core.binding import BindingPrediction
139+
from dogneo.core.ranking import build_candidates
140+
141+
protein_db = ProteinDatabase()
142+
# TODO: accept --protein-db CLI flag for standalone usage
143+
peptides_by_variant: dict[str, list] = {}
144+
predictions_by_peptide: dict[str, list] = {}
145+
146+
for v in coding:
147+
peps = generate_peptides(v, protein_db, lengths=mhci_lens)
148+
if peps:
149+
peptides_by_variant[v.variant_id] = peps
150+
151+
if not peptides_by_variant:
152+
click.secho(
153+
"⚠️ No peptides generated — this likely means no canine protein DB "
154+
"was loaded. Use the full Snakemake pipeline (dogneo run) or provide "
155+
"a pre-built candidates JSON to the report command.",
156+
fg="yellow",
157+
)
158+
159+
# Step 4: Ranking
137160
click.echo("📊 Scoring and ranking candidates...")
138-
# In a full implementation, this would chain through binding prediction
139-
# For now, we create candidates from variant data
140-
candidates: list[NeoantigenCandidate] = []
141-
click.echo(f" {len(candidates)} candidates ranked")
161+
candidates = build_candidates(coding, peptides_by_variant, predictions_by_peptide)
162+
163+
if allele_list and candidates:
164+
ranked = rank_candidates(candidates)
165+
click.echo(f" {len(ranked)} candidates ranked")
166+
else:
167+
ranked = candidates
168+
click.echo(f" {len(ranked)} candidates (unranked — no alleles or binding data)")
142169

143170
# Step 5: Export
144171
if "tsv" in format_list:
@@ -183,9 +210,41 @@ def report(input_path: str, fmt: str, output: str, llm_tier: str) -> None:
183210
with open(input_path) as f:
184211
data = _json.load(f)
185212

186-
# Reconstruct candidates (simplified)
187-
click.echo(f"📄 Generating {fmt} report...")
188-
click.echo(f" Input: {data.get('metadata', {}).get('total_candidates', '?')} candidates")
213+
total = data.get("metadata", {}).get("total_candidates", "?")
214+
sample_id = data.get("metadata", {}).get("sample_id", "UNKNOWN")
215+
click.echo(f"📄 Generating {fmt} report from {total} candidates...")
216+
217+
from dogneo.report.generator import ReportGenerator
218+
from dogneo.config import LLMConfig
219+
from dogneo.llm.router import LLMRouter
220+
221+
llm_router = None
222+
if llm_tier != "none":
223+
llm_config = LLMConfig(default_tier=llm_tier)
224+
llm_router = LLMRouter(config=llm_config)
225+
226+
gen = ReportGenerator(llm_router=llm_router)
227+
output_path = Path(output)
228+
229+
# The JSON's "candidates" list already has serialized candidate dicts
230+
candidate_dicts = data.get("candidates", [])
231+
232+
if fmt == "html":
233+
gen.generate_html(
234+
[], sample_id,
235+
parameters=data.get("metadata", {}).get("parameters", {}),
236+
alleles=data.get("metadata", {}).get("alleles", []),
237+
output_path=output_path,
238+
pre_rendered_candidates=candidate_dicts,
239+
)
240+
else:
241+
gen.generate_markdown(
242+
[], sample_id,
243+
parameters=data.get("metadata", {}).get("parameters", {}),
244+
output_path=output_path,
245+
pre_rendered_candidates=candidate_dicts,
246+
)
247+
189248
click.secho(f"✅ Report written to: {output}", fg="green")
190249

191250

dogneo/core/ranking.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ class NeoantigenCandidate:
3535
peptide: MutantPeptide
3636
binding: BindingPrediction
3737
expression_tpm: float = 0.0
38+
wt_binding_nm: float = 0.0 # WT binding affinity for agretopicity (0 = unknown)
3839
composite_score: float = 0.0
3940
rank: int = 0
4041
score_components: dict[str, float] = field(default_factory=dict)
@@ -189,8 +190,13 @@ def rank_candidates(
189190
candidate.peptide.mut_sequence,
190191
)
191192

192-
# Agretopicity (placeholder: would need WT binding prediction)
193-
components["agretopicity"] = 0.5 # Default neutral
193+
# Agretopicity: compare WT vs mutant binding affinity
194+
if candidate.wt_binding_nm > 0:
195+
components["agretopicity"] = _score_agretopicity(
196+
candidate.wt_binding_nm, candidate.binding.affinity_nm,
197+
)
198+
else:
199+
components["agretopicity"] = 0.5 # Neutral when WT binding unknown
194200

195201
# Caller agreement
196202
components["caller_agreement"] = _score_caller_agreement(

dogneo/core/variants.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ def _extract_vep_annotation(csq_str: str) -> dict[str, str]:
124124
"effect": parts[1],
125125
"gene": parts[3],
126126
"transcript_id": parts[6],
127-
"hgvs_c": parts.get(10, "") if len(parts) > 10 else "",
127+
"hgvs_c": parts[10] if len(parts) > 10 else "",
128128
"hgvs_p": parts[11] if len(parts) > 11 else "",
129129
}
130130

dogneo/report/generator.py

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ def generate_html(
142142
alleles: list[str] | None = None,
143143
output_path: str | Path | None = None,
144144
top_n: int = 50,
145+
pre_rendered_candidates: list[dict] | None = None,
145146
) -> str:
146147
"""Generate an HTML report from ranked candidates.
147148
@@ -159,7 +160,10 @@ def generate_html(
159160
from jinja2 import Template
160161

161162
# Prepare candidate dicts for template
162-
candidate_dicts = [c.to_dict() for c in candidates[:top_n]]
163+
if pre_rendered_candidates is not None:
164+
candidate_dicts = pre_rendered_candidates[:top_n]
165+
else:
166+
candidate_dicts = [c.to_dict() for c in candidates[:top_n]]
163167

164168
# Generate AI summary if router available
165169
ai_summary = ""
@@ -195,6 +199,9 @@ def generate_markdown(
195199
candidates: list[NeoantigenCandidate],
196200
sample_id: str,
197201
top_n: int = 20,
202+
parameters: dict[str, Any] | None = None,
203+
output_path: str | Path | None = None,
204+
pre_rendered_candidates: list[dict] | None = None,
198205
) -> str:
199206
"""Generate a Markdown summary of top candidates.
200207
@@ -220,14 +227,25 @@ def generate_markdown(
220227
"|---|------|----------|---------|--------|----------|-----|-------|",
221228
]
222229

223-
for c in candidates[:top_n]:
224-
d = c.to_dict()
230+
if pre_rendered_candidates is not None:
231+
candidate_dicts = pre_rendered_candidates[:top_n]
232+
else:
233+
candidate_dicts = [c.to_dict() for c in candidates[:top_n]]
234+
235+
for d in candidate_dicts:
225236
lines.append(
226-
f"| {d['rank']} | {d['gene']} | {d['mutation']} | "
227-
f"`{d['mutant_peptide']}` | {d['allele']} | "
228-
f"{d['binding_affinity_nm']:.1f} | {d['expression_tpm']:.1f} | "
229-
f"{d['composite_score']:.4f} |"
237+
f"| {d.get('rank', '-')} | {d.get('gene', '')} | {d.get('mutation', '')} | "
238+
f"`{d.get('mutant_peptide', '')}` | {d.get('allele', '')} | "
239+
f"{float(d.get('binding_affinity_nm', 0)):.1f} | {float(d.get('expression_tpm', 0)):.1f} | "
240+
f"{float(d.get('composite_score', 0)):.4f} |"
230241
)
231242

232-
lines.extend(["", f"*Total candidates: {len(candidates)}*"])
233-
return "\n".join(lines)
243+
total = len(pre_rendered_candidates) if pre_rendered_candidates else len(candidates)
244+
lines.extend(["", f"*Total candidates: {total}*"])
245+
md = "\n".join(lines)
246+
247+
if output_path:
248+
Path(output_path).write_text(md, encoding="utf-8")
249+
logger.info("Markdown report written to: %s", output_path)
250+
251+
return md

pyproject.toml

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[build-system]
22
requires = ["setuptools>=68.0", "wheel"]
3-
build-backend = "setuptools.backends"
3+
build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "dogneo"
@@ -25,10 +25,8 @@ classifiers = [
2525
]
2626

2727
dependencies = [
28-
"pysam>=0.22.0",
2928
"pandas>=2.0",
3029
"biopython>=1.82",
31-
"pyvcf3>=1.0.3",
3230
"click>=8.1",
3331
"jinja2>=3.1",
3432
"pyyaml>=6.0",
@@ -37,12 +35,15 @@ dependencies = [
3735

3836
[project.optional-dependencies]
3937
llm = [
40-
"langchain-core>=0.2",
4138
"llama-cpp-python>=0.2.50",
4239
"openai>=1.10",
4340
"anthropic>=0.25",
4441
"google-generativeai>=0.5",
4542
]
43+
bio = [
44+
"pysam>=0.22.0",
45+
"pyvcf3>=1.0.3",
46+
]
4647
pipeline = [
4748
"snakemake>=8.0",
4849
]
@@ -52,15 +53,15 @@ dev = [
5253
"ruff>=0.3",
5354
"mypy>=1.8",
5455
]
55-
all = ["dogneo[llm,pipeline,dev]"]
56+
all = ["dogneo[llm,bio,pipeline,dev]"]
5657

5758
[project.scripts]
5859
dogneo = "dogneo.cli:main"
5960

6061
[project.urls]
61-
Homepage = "https://github.com/dog-mrna-sos/dogneo"
62-
Documentation = "https://github.com/dog-mrna-sos/dogneo#readme"
63-
Issues = "https://github.com/dog-mrna-sos/dogneo/issues"
62+
Homepage = "https://github.com/ImL1s/dogneo"
63+
Documentation = "https://github.com/ImL1s/dogneo#readme"
64+
Issues = "https://github.com/ImL1s/dogneo/issues"
6465

6566
[tool.setuptools.packages.find]
6667
include = ["dogneo*"]

0 commit comments

Comments
 (0)