From 7175a4f9fea214200eb6c04cf5cbab7bded4addf Mon Sep 17 00:00:00 2001 From: Charlie Date: Wed, 28 Jan 2026 12:14:53 -0500 Subject: [PATCH 1/5] Add species pages for treefiles --- app/app.py | 39 ++++++++++++++++++++ app/nl_query.py | 4 +- app/templates/show_assembly.html | 10 ++++- app/templates/show_isolate.html | 10 ++++- app/templates/show_species.html | 39 ++++++++++++++++++++ app/templates/show_taxonomic_assignment.html | 10 ++++- 6 files changed, 108 insertions(+), 4 deletions(-) create mode 100644 app/templates/show_species.html diff --git a/app/app.py b/app/app.py index 3be3ae6..41ce1a8 100644 --- a/app/app.py +++ b/app/app.py @@ -1,5 +1,6 @@ import csv import os +from typing import Optional from app import __version__, get_db_last_sync from flask import ( Flask, @@ -39,6 +40,14 @@ app.secret_key = os.urandom(12) IS_DEV_SITE = os.environ.get("MARC_DEV", "").lower() == "true" +MARC_TREE_FP = os.environ.get("MARC_TREE_FP") + + +def treefile_for_species(species_name: str) -> Optional[str]: + if not species_name: + return None + slug = "_".join(species_name.lower().split()) + return f"{slug}.treefile" @app.context_processor @@ -380,6 +389,36 @@ def show_antimicrobial(antimicrobial_id: int): ) +@app.route("/species/") +def show_species(species_name: str): + assignment_count = ( + db.session.query(func.count(TaxonomicAssignment.id)) + .filter(TaxonomicAssignment.classification == species_name) + .scalar() + ) + treefile_name = treefile_for_species(species_name) + tree_content = None + tree_path = None + tree_error = None + if MARC_TREE_FP and treefile_name: + candidate = Path(MARC_TREE_FP) / treefile_name + tree_path = str(candidate) + if candidate.is_file(): + try: + tree_content = candidate.read_text() + except Exception as exc: + tree_error = str(exc) + return render_template( + "show_species.html", + species_name=species_name, + assignment_count=assignment_count, + tree_path=tree_path, + tree_content=tree_content, + tree_error=tree_error, + tree_root=MARC_TREE_FP, + ) + + @app.route("/assembly/") def show_assembly(assembly_id: int): assemblies = get_assemblies(db.session, id=assembly_id) diff --git a/app/nl_query.py b/app/nl_query.py index 606e381..2b3b9fe 100644 --- a/app/nl_query.py +++ b/app/nl_query.py @@ -53,7 +53,8 @@ class State(TypedDict): ] ) -GENERATE_QUERY_PROMPT = lambda user_input: f""" +GENERATE_QUERY_PROMPT = ( + lambda user_input: f""" ``` SYSTEM Given an input question, create a syntactically correct SQLite3 query to run to help find the answer. Unless the user specifies in his question a specific number of examples they wish to obtain, you can return all the results that match the question. @@ -69,6 +70,7 @@ class State(TypedDict): {user_input} ``` """ +) llm = ChatOpenAI( model="gpt-4o", diff --git a/app/templates/show_assembly.html b/app/templates/show_assembly.html index c71952e..9f7d10c 100644 --- a/app/templates/show_assembly.html +++ b/app/templates/show_assembly.html @@ -117,7 +117,15 @@

Taxonomic Assignments

{% for assignment in assignments %} - {{ assignment.classification or '—' }} + + {% if assignment.classification %} + + {{ assignment.classification }} + + {% else %} + — + {% endif %} + {{ assignment.comment or '—' }} {{ assignment.tool or '—' }} diff --git a/app/templates/show_isolate.html b/app/templates/show_isolate.html index 903e38e..91efe62 100644 --- a/app/templates/show_isolate.html +++ b/app/templates/show_isolate.html @@ -21,7 +21,15 @@

Isolate {{ isolate.sample_id }}

{{ isolate.specimen_id }}
Suspected Organism
-
{{ isolate.suspected_organism or '—' }}
+
+ {% if isolate.suspected_organism %} + + {{ isolate.suspected_organism }} + + {% else %} + — + {% endif %} +
Special Collection
{{ isolate.special_collection or '—' }}
diff --git a/app/templates/show_species.html b/app/templates/show_species.html new file mode 100644 index 0000000..4955811 --- /dev/null +++ b/app/templates/show_species.html @@ -0,0 +1,39 @@ +{% extends 'base.html' %} + +{% block body %} +
+ + Back to taxonomic assignments + + +
+
+

{{ species_name }}

+
+
+
+
Matching taxonomic assignments
+
{{ assignment_count }}
+
+
+
+ +
+
+

Tree

+
+
+ {% if tree_error %} +

Unable to read treefile: {{ tree_error }}

+ {% elif tree_content %} +

Source: {{ tree_path }}

+
{{ tree_content }}
+ {% elif tree_root %} +

No treefile found for this species.

+ {% else %} +

Set the MARC_TREE_FP environment variable to load treefiles.

+ {% endif %} +
+
+
+{% endblock %} diff --git a/app/templates/show_taxonomic_assignment.html b/app/templates/show_taxonomic_assignment.html index 2bbd305..d4f1814 100644 --- a/app/templates/show_taxonomic_assignment.html +++ b/app/templates/show_taxonomic_assignment.html @@ -22,7 +22,15 @@

Taxonomic Assignment for Assembly {{ assembly_id }}

{{ assignment.tool or '—' }}
Classification
-
{{ assignment.classification or '—' }}
+
+ {% if assignment.classification %} + + {{ assignment.classification }} + + {% else %} + — + {% endif %} +
Comment
{{ assignment.comment or '—' }}
From 3725d54efa0b0fd97af45979d18f3fff42d10499 Mon Sep 17 00:00:00 2001 From: Charlie Date: Wed, 28 Jan 2026 12:31:02 -0500 Subject: [PATCH 2/5] Render species tree visualizations --- app/templates/browse_isolates.html | 11 +++++++- .../browse_taxonomic_assignments.html | 11 +++++++- app/templates/show_species.html | 26 ++++++++++++++++++- 3 files changed, 45 insertions(+), 3 deletions(-) diff --git a/app/templates/browse_isolates.html b/app/templates/browse_isolates.html index 5b550c3..028cb62 100644 --- a/app/templates/browse_isolates.html +++ b/app/templates/browse_isolates.html @@ -27,6 +27,7 @@

Isolates

$(document).ready(function () { /* Initialize the DataTable with server-side processing */ const isolateUrl = "{{ url_for('show_isolate', isolate_id='') }}"; + const speciesUrl = "{{ url_for('show_species', species_name='') }}"; const renderCollectionYear = (value, type) => { if (!value) { return type === 'sort' || type === 'type' ? null : ''; @@ -59,7 +60,15 @@

Isolates

{ data: 'received_date', render: function(value, type) { return renderCollectionYear(value, type); } }, { data: 'subject_id' }, { data: 'specimen_id' }, - { data: 'suspected_organism' } + { data: 'suspected_organism', render: function(value, type) { + if (type !== 'display') { + return value; + } + if (!value) { + return ''; + } + return '' + value + ''; + } } ]); /* Move search box to bottom of summary area */ diff --git a/app/templates/browse_taxonomic_assignments.html b/app/templates/browse_taxonomic_assignments.html index f41626c..d686b80 100644 --- a/app/templates/browse_taxonomic_assignments.html +++ b/app/templates/browse_taxonomic_assignments.html @@ -25,11 +25,20 @@

Taxonomic Assignments

+ + + + {% elif tree_root %}

No treefile found for this species.

{% else %} From 86b0dd471a33d95ba5a01e3c5a1872b5d2aa12e7 Mon Sep 17 00:00:00 2001 From: Ulthran Date: Wed, 28 Jan 2026 12:57:26 -0500 Subject: [PATCH 3/5] Clean up tree display --- .gitignore | 1 + app/templates/show_species.html | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 9233dd9..cc80c1a 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ *.sqlite3 *.sqlite last_sync +trees/ # Byte-compiled / optimized / DLL files __pycache__/ diff --git a/app/templates/show_species.html b/app/templates/show_species.html index c6c0892..9d2c904 100644 --- a/app/templates/show_species.html +++ b/app/templates/show_species.html @@ -2,9 +2,9 @@ {% block head %} {% endblock %} @@ -43,6 +43,7 @@

Tree