lobster-transcriptomics 1.2.0__tar.gz → 1.2.2__tar.gz
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.
- {lobster_transcriptomics-1.2.0/lobster_transcriptomics.egg-info → lobster_transcriptomics-1.2.2}/PKG-INFO +1 -1
- {lobster_transcriptomics-1.2.0 → lobster_transcriptomics-1.2.2}/lobster/agents/transcriptomics/annotation_expert.py +6 -0
- {lobster_transcriptomics-1.2.0 → lobster_transcriptomics-1.2.2}/lobster/agents/transcriptomics/de_analysis_expert.py +87 -30
- {lobster_transcriptomics-1.2.0 → lobster_transcriptomics-1.2.2}/lobster/agents/transcriptomics/prompts.py +55 -2
- {lobster_transcriptomics-1.2.0 → lobster_transcriptomics-1.2.2}/lobster/agents/transcriptomics/shared_tools.py +152 -38
- {lobster_transcriptomics-1.2.0 → lobster_transcriptomics-1.2.2}/lobster/agents/transcriptomics/transcriptomics_expert.py +132 -27
- {lobster_transcriptomics-1.2.0 → lobster_transcriptomics-1.2.2}/lobster/services/analysis/enhanced_singlecell_service.py +186 -100
- {lobster_transcriptomics-1.2.0 → lobster_transcriptomics-1.2.2}/lobster/services/quality/preprocessing_service.py +54 -17
- {lobster_transcriptomics-1.2.0 → lobster_transcriptomics-1.2.2}/lobster/services/quality/quality_service.py +42 -0
- {lobster_transcriptomics-1.2.0 → lobster_transcriptomics-1.2.2/lobster_transcriptomics.egg-info}/PKG-INFO +1 -1
- {lobster_transcriptomics-1.2.0 → lobster_transcriptomics-1.2.2}/pyproject.toml +1 -1
- {lobster_transcriptomics-1.2.0 → lobster_transcriptomics-1.2.2}/LICENSE +0 -0
- {lobster_transcriptomics-1.2.0 → lobster_transcriptomics-1.2.2}/README.md +0 -0
- {lobster_transcriptomics-1.2.0 → lobster_transcriptomics-1.2.2}/lobster/agents/transcriptomics/__init__.py +0 -0
- {lobster_transcriptomics-1.2.0 → lobster_transcriptomics-1.2.2}/lobster/agents/transcriptomics/config.py +0 -0
- {lobster_transcriptomics-1.2.0 → lobster_transcriptomics-1.2.2}/lobster/agents/transcriptomics/py.typed +0 -0
- {lobster_transcriptomics-1.2.0 → lobster_transcriptomics-1.2.2}/lobster/agents/transcriptomics/state.py +0 -0
- {lobster_transcriptomics-1.2.0 → lobster_transcriptomics-1.2.2}/lobster/figure_template_providers/sc.py +0 -0
- {lobster_transcriptomics-1.2.0 → lobster_transcriptomics-1.2.2}/lobster/services/analysis/bulk_preprocessing_service.py +0 -0
- {lobster_transcriptomics-1.2.0 → lobster_transcriptomics-1.2.2}/lobster/services/analysis/differential_formula_service.py +0 -0
- {lobster_transcriptomics-1.2.0 → lobster_transcriptomics-1.2.2}/lobster/services/analysis/pseudobulk_service.py +0 -0
- {lobster_transcriptomics-1.2.0 → lobster_transcriptomics-1.2.2}/lobster/services/metadata/manual_annotation_service.py +0 -0
- {lobster_transcriptomics-1.2.0 → lobster_transcriptomics-1.2.2}/lobster/services/templates/annotation_templates.py +0 -0
- {lobster_transcriptomics-1.2.0 → lobster_transcriptomics-1.2.2}/lobster/services/visualization/bulk_visualization_service.py +0 -0
- {lobster_transcriptomics-1.2.0 → lobster_transcriptomics-1.2.2}/lobster_transcriptomics.egg-info/SOURCES.txt +0 -0
- {lobster_transcriptomics-1.2.0 → lobster_transcriptomics-1.2.2}/lobster_transcriptomics.egg-info/dependency_links.txt +0 -0
- {lobster_transcriptomics-1.2.0 → lobster_transcriptomics-1.2.2}/lobster_transcriptomics.egg-info/entry_points.txt +0 -0
- {lobster_transcriptomics-1.2.0 → lobster_transcriptomics-1.2.2}/lobster_transcriptomics.egg-info/requires.txt +0 -0
- {lobster_transcriptomics-1.2.0 → lobster_transcriptomics-1.2.2}/lobster_transcriptomics.egg-info/top_level.txt +0 -0
- {lobster_transcriptomics-1.2.0 → lobster_transcriptomics-1.2.2}/setup.cfg +0 -0
|
@@ -54,6 +54,7 @@ from lobster.config.inference_context import (
|
|
|
54
54
|
from lobster.config.llm_factory import create_llm
|
|
55
55
|
from lobster.config.settings import get_settings
|
|
56
56
|
from lobster.core.component_registry import component_registry
|
|
57
|
+
from lobster.core.export_scope import WORKSPACE_EXPORT_MESSAGE, outside_workspace
|
|
57
58
|
from lobster.core.figures.export_guard import MANAGED_EXPORT_MESSAGE, export_refused
|
|
58
59
|
from lobster.core.runtime.data_manager import DataManagerV2
|
|
59
60
|
from lobster.services.analysis.enhanced_singlecell_service import (
|
|
@@ -1067,12 +1068,17 @@ def annotation_expert(
|
|
|
1067
1068
|
output_path = Path(output_filename)
|
|
1068
1069
|
if not output_path.is_absolute():
|
|
1069
1070
|
output_path = data_manager.exports_dir / output_path
|
|
1071
|
+
# The destination stays inside the workspace (or a listed export root).
|
|
1072
|
+
if outside_workspace(data_manager.workspace_path, output_path):
|
|
1073
|
+
return WORKSPACE_EXPORT_MESSAGE
|
|
1070
1074
|
# Figures are saved only through the trusted save step: check the
|
|
1071
1075
|
# caller's (unresolved) destination before anything is created.
|
|
1072
1076
|
if export_refused(data_manager.workspace_path, output_path):
|
|
1073
1077
|
return MANAGED_EXPORT_MESSAGE
|
|
1074
1078
|
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
1075
1079
|
output_filename = str(output_path.resolve())
|
|
1080
|
+
if outside_workspace(data_manager.workspace_path, output_filename):
|
|
1081
|
+
return WORKSPACE_EXPORT_MESSAGE
|
|
1076
1082
|
if export_refused(
|
|
1077
1083
|
data_manager.workspace_path, output_path, output_filename
|
|
1078
1084
|
):
|
|
@@ -53,6 +53,15 @@ from lobster.core import (
|
|
|
53
53
|
InsufficientCellsError,
|
|
54
54
|
PseudobulkError,
|
|
55
55
|
)
|
|
56
|
+
from lobster.core.export_scope import (
|
|
57
|
+
WORKSPACE_EXPORT_MESSAGE,
|
|
58
|
+
InvalidModalityNameError,
|
|
59
|
+
WorkspaceExportError,
|
|
60
|
+
assert_inside_workspace,
|
|
61
|
+
check_modality_name,
|
|
62
|
+
outside_workspace,
|
|
63
|
+
plain_name_part,
|
|
64
|
+
)
|
|
56
65
|
from lobster.core.figures.errors import ManagedPathError
|
|
57
66
|
from lobster.core.figures.export_dirs import assert_dirs_allowed
|
|
58
67
|
from lobster.core.figures.export_guard import (
|
|
@@ -89,6 +98,15 @@ _BULK_SERVICE_SETUP = (
|
|
|
89
98
|
)
|
|
90
99
|
|
|
91
100
|
|
|
101
|
+
def _de_result_name(modality_name: str, group1, group2) -> str:
|
|
102
|
+
"""The DE result modality name: group labels are data values, so each is encoded
|
|
103
|
+
as part of one plain name (two labels never collide); refused (fixed reason)
|
|
104
|
+
before any analysis if the name is still not plain, for example too long."""
|
|
105
|
+
return check_modality_name(
|
|
106
|
+
f"{modality_name}_de_{plain_name_part(group1)}_vs_{plain_name_part(group2)}"
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
|
|
92
110
|
def _is_pseudobulk(adata) -> bool:
|
|
93
111
|
"""True when the modality carries a pseudobulk or formula design (``uns`` only)."""
|
|
94
112
|
return (
|
|
@@ -1091,7 +1109,9 @@ def de_analysis_expert(
|
|
|
1091
1109
|
status = (
|
|
1092
1110
|
"OK"
|
|
1093
1111
|
if count >= 4
|
|
1094
|
-
else "LOW POWER"
|
|
1112
|
+
else "LOW POWER"
|
|
1113
|
+
if count >= 3
|
|
1114
|
+
else "INSUFFICIENT"
|
|
1095
1115
|
)
|
|
1096
1116
|
response += f"\n- {group}: {count} replicates ({status})"
|
|
1097
1117
|
response += _replicate_summary_text(replicate_validation)
|
|
@@ -1174,21 +1194,26 @@ def de_analysis_expert(
|
|
|
1174
1194
|
# Get the modality
|
|
1175
1195
|
adata = data_manager.get_modality(modality_name)
|
|
1176
1196
|
|
|
1197
|
+
# The result name carries the group labels (data values, which may hold
|
|
1198
|
+
# '/'): encoded into one plain name, checked before any analysis.
|
|
1199
|
+
de_modality_name = None
|
|
1200
|
+
if not (_is_pseudobulk(adata) and method == "deseq2"):
|
|
1201
|
+
de_modality_name = _de_result_name(modality_name, group1, group2)
|
|
1202
|
+
|
|
1177
1203
|
# Every save target of the branch that will run is checked before the
|
|
1178
1204
|
# data changes or any analysis runs.
|
|
1179
1205
|
if save_result:
|
|
1180
1206
|
if _is_pseudobulk(adata) and method == "deseq2":
|
|
1181
|
-
|
|
1182
|
-
data_manager.
|
|
1183
|
-
data_manager.exports_dir / f"{modality_name}_de_results.csv",
|
|
1207
|
+
results_csv = (
|
|
1208
|
+
data_manager.exports_dir / f"{modality_name}_de_results.csv"
|
|
1184
1209
|
)
|
|
1210
|
+
assert_inside_workspace(data_manager.workspace_path, results_csv)
|
|
1211
|
+
assert_export_allowed(data_manager.workspace_path, results_csv)
|
|
1185
1212
|
data_manager._assert_save_allowed(
|
|
1186
1213
|
f"{modality_name}_with_de_results.h5ad"
|
|
1187
1214
|
)
|
|
1188
1215
|
else:
|
|
1189
|
-
data_manager._assert_save_allowed(
|
|
1190
|
-
f"{modality_name}_de_{group1}_vs_{group2}.h5ad"
|
|
1191
|
-
)
|
|
1216
|
+
data_manager._assert_save_allowed(f"{de_modality_name}.h5ad")
|
|
1192
1217
|
|
|
1193
1218
|
logger.info(
|
|
1194
1219
|
f"Running DE analysis on '{modality_name}': "
|
|
@@ -1404,8 +1429,7 @@ def de_analysis_expert(
|
|
|
1404
1429
|
de_stats["de_results_key"],
|
|
1405
1430
|
"adata_de" if internal_method == "deseq2_like" else "adata",
|
|
1406
1431
|
)
|
|
1407
|
-
# Save as new modality
|
|
1408
|
-
de_modality_name = f"{modality_name}_de_{group1}_vs_{group2}"
|
|
1432
|
+
# Save as new modality (name derived and checked before the analysis)
|
|
1409
1433
|
data_manager.store_modality(
|
|
1410
1434
|
name=de_modality_name,
|
|
1411
1435
|
adata=adata_de,
|
|
@@ -1414,7 +1438,7 @@ def de_analysis_expert(
|
|
|
1414
1438
|
)
|
|
1415
1439
|
|
|
1416
1440
|
if save_result:
|
|
1417
|
-
save_path = f"{
|
|
1441
|
+
save_path = f"{de_modality_name}.h5ad"
|
|
1418
1442
|
data_manager.save_modality(de_modality_name, save_path)
|
|
1419
1443
|
|
|
1420
1444
|
data_manager.log_tool_usage(
|
|
@@ -1437,13 +1461,13 @@ def de_analysis_expert(
|
|
|
1437
1461
|
|
|
1438
1462
|
**Analysis Results:**
|
|
1439
1463
|
- Comparison: {group1} ({group1_count} samples) vs {group2} ({group2_count} samples)
|
|
1440
|
-
- Method: {de_stats[
|
|
1441
|
-
- Genes tested: {de_stats[
|
|
1442
|
-
- Significant genes (padj < {alpha}): {de_stats[
|
|
1464
|
+
- Method: {de_stats["method"]}
|
|
1465
|
+
- Genes tested: {de_stats["n_genes_tested"]:,}
|
|
1466
|
+
- Significant genes (padj < {alpha}): {de_stats["n_significant_genes"]:,}
|
|
1443
1467
|
|
|
1444
1468
|
**Differential Expression Summary:**
|
|
1445
|
-
- Upregulated in {group2}: {de_stats[
|
|
1446
|
-
- Downregulated in {group2}: {de_stats[
|
|
1469
|
+
- Upregulated in {group2}: {de_stats["n_upregulated"]} genes
|
|
1470
|
+
- Downregulated in {group2}: {de_stats["n_downregulated"]} genes
|
|
1447
1471
|
|
|
1448
1472
|
**Top Upregulated Genes:**
|
|
1449
1473
|
{chr(10).join([f"- {gene}" for gene in de_stats["top_upregulated"][:5]])}
|
|
@@ -1471,6 +1495,12 @@ def de_analysis_expert(
|
|
|
1471
1495
|
analysis_results["details"]["differential_expression"] = response
|
|
1472
1496
|
return response
|
|
1473
1497
|
|
|
1498
|
+
except WorkspaceExportError:
|
|
1499
|
+
# A save target is outside the workspace: nothing was saved.
|
|
1500
|
+
return WORKSPACE_EXPORT_MESSAGE
|
|
1501
|
+
except InvalidModalityNameError as e:
|
|
1502
|
+
# The derived result name is not one plain name (fixed reason).
|
|
1503
|
+
return f"Error: {e}"
|
|
1474
1504
|
except ManagedPathError:
|
|
1475
1505
|
# A save target is inside the managed figure folder: nothing was saved.
|
|
1476
1506
|
return MANAGED_EXPORT_MESSAGE
|
|
@@ -1897,7 +1927,9 @@ def de_analysis_expert(
|
|
|
1897
1927
|
status = (
|
|
1898
1928
|
"OK"
|
|
1899
1929
|
if count >= 4
|
|
1900
|
-
else "LOW POWER"
|
|
1930
|
+
else "LOW POWER"
|
|
1931
|
+
if count >= 3
|
|
1932
|
+
else "INSUFFICIENT"
|
|
1901
1933
|
)
|
|
1902
1934
|
response += f"- {group}: {count} replicates ({status})\n"
|
|
1903
1935
|
response += _replicate_summary_text(replicate_validation)
|
|
@@ -1989,11 +2021,12 @@ def de_analysis_expert(
|
|
|
1989
2021
|
|
|
1990
2022
|
# Both save targets are checked before any analysis or store.
|
|
1991
2023
|
if save_results:
|
|
1992
|
-
|
|
1993
|
-
data_manager.workspace_path,
|
|
2024
|
+
formula_csv = (
|
|
1994
2025
|
data_manager.exports_dir
|
|
1995
|
-
/ f"{pseudobulk_modality}_formula_de_results.csv"
|
|
2026
|
+
/ f"{pseudobulk_modality}_formula_de_results.csv"
|
|
1996
2027
|
)
|
|
2028
|
+
assert_inside_workspace(data_manager.workspace_path, formula_csv)
|
|
2029
|
+
assert_export_allowed(data_manager.workspace_path, formula_csv)
|
|
1997
2030
|
data_manager._assert_save_allowed(
|
|
1998
2031
|
f"{pseudobulk_modality}_with_formula_results.h5ad"
|
|
1999
2032
|
)
|
|
@@ -2192,6 +2225,9 @@ def de_analysis_expert(
|
|
|
2192
2225
|
|
|
2193
2226
|
return response
|
|
2194
2227
|
|
|
2228
|
+
except WorkspaceExportError:
|
|
2229
|
+
# A save target is outside the workspace: nothing was saved.
|
|
2230
|
+
return WORKSPACE_EXPORT_MESSAGE
|
|
2195
2231
|
except ManagedPathError:
|
|
2196
2232
|
# A save target is inside the managed figure folder: nothing was saved.
|
|
2197
2233
|
return MANAGED_EXPORT_MESSAGE
|
|
@@ -3034,6 +3070,9 @@ def de_analysis_expert(
|
|
|
3034
3070
|
output_path_obj = Path(output_path)
|
|
3035
3071
|
if not output_path_obj.is_absolute():
|
|
3036
3072
|
output_path_obj = data_manager.exports_dir / output_path_obj
|
|
3073
|
+
# The destination stays inside the workspace (or a listed export root).
|
|
3074
|
+
if outside_workspace(data_manager.workspace_path, output_path_obj):
|
|
3075
|
+
return WORKSPACE_EXPORT_MESSAGE
|
|
3037
3076
|
# Figures are saved only through the trusted save step: check the
|
|
3038
3077
|
# caller's (unresolved) destination, and every folder its recursive
|
|
3039
3078
|
# creation can make, before anything is created.
|
|
@@ -3045,6 +3084,8 @@ def de_analysis_expert(
|
|
|
3045
3084
|
return MANAGED_EXPORT_MESSAGE
|
|
3046
3085
|
output_path_obj.parent.mkdir(parents=True, exist_ok=True)
|
|
3047
3086
|
output_path = str(output_path_obj.resolve())
|
|
3087
|
+
if outside_workspace(data_manager.workspace_path, output_path):
|
|
3088
|
+
return WORKSPACE_EXPORT_MESSAGE
|
|
3048
3089
|
if export_refused(
|
|
3049
3090
|
data_manager.workspace_path, output_path_obj, output_path
|
|
3050
3091
|
):
|
|
@@ -3080,7 +3121,7 @@ def de_analysis_expert(
|
|
|
3080
3121
|
"results_df = results_df.sort_values({{ sort_by | string | repr }})\n"
|
|
3081
3122
|
"results_df.to_{{ output_format }}({{ output_path | string | repr }}, index=True)"
|
|
3082
3123
|
),
|
|
3083
|
-
imports=["pandas"],
|
|
3124
|
+
imports=["import pandas as pd"],
|
|
3084
3125
|
)
|
|
3085
3126
|
|
|
3086
3127
|
data_manager.log_tool_usage(
|
|
@@ -3107,8 +3148,8 @@ def de_analysis_expert(
|
|
|
3107
3148
|
- Sorted by: {sort_by}
|
|
3108
3149
|
- Output: {output_path}
|
|
3109
3150
|
|
|
3110
|
-
**Columns:** {
|
|
3111
|
-
**Filtered only:** {
|
|
3151
|
+
**Columns:** {", ".join(results_df.columns.tolist()[:8])}{"..." if len(results_df.columns) > 8 else ""}
|
|
3152
|
+
**Filtered only:** {"Yes" if filtered_only else "No"}"""
|
|
3112
3153
|
|
|
3113
3154
|
return response
|
|
3114
3155
|
|
|
@@ -3189,6 +3230,9 @@ def de_analysis_expert(
|
|
|
3189
3230
|
if group2 not in available_groups_str:
|
|
3190
3231
|
return f"Group '{group2}' not found in '{group_key}'. Available: {available_groups}"
|
|
3191
3232
|
|
|
3233
|
+
# The result name carries the group labels: encoded and checked first.
|
|
3234
|
+
de_modality_name = _de_result_name(modality_name, group1, group2)
|
|
3235
|
+
|
|
3192
3236
|
# Ensure string type matching
|
|
3193
3237
|
if group1 not in available_groups:
|
|
3194
3238
|
adata.obs[group_key] = adata.obs[group_key].astype(str)
|
|
@@ -3223,8 +3267,7 @@ def de_analysis_expert(
|
|
|
3223
3267
|
if refusal:
|
|
3224
3268
|
return refusal
|
|
3225
3269
|
adata_de.uns["de_current_results_key"] = de_stats["de_results_key"]
|
|
3226
|
-
# Store result modality
|
|
3227
|
-
de_modality_name = f"{modality_name}_de_{group1}_vs_{group2}"
|
|
3270
|
+
# Store result modality (name derived and checked before the analysis)
|
|
3228
3271
|
data_manager.store_modality(
|
|
3229
3272
|
name=de_modality_name,
|
|
3230
3273
|
adata=adata_de,
|
|
@@ -3292,7 +3335,7 @@ def de_analysis_expert(
|
|
|
3292
3335
|
**Method:** DESeq2-like bulk differential expression
|
|
3293
3336
|
|
|
3294
3337
|
**Results:**
|
|
3295
|
-
- Genes tested: {de_stats.get(
|
|
3338
|
+
- Genes tested: {de_stats.get("n_genes_tested", "N/A"):,}
|
|
3296
3339
|
- Significant genes (padj < {alpha}): {n_sig:,}
|
|
3297
3340
|
- Upregulated in {group2}: {n_up:,}
|
|
3298
3341
|
- Downregulated in {group2}: {n_down:,}
|
|
@@ -3320,6 +3363,9 @@ def de_analysis_expert(
|
|
|
3320
3363
|
|
|
3321
3364
|
return response
|
|
3322
3365
|
|
|
3366
|
+
except InvalidModalityNameError as e:
|
|
3367
|
+
# The derived result name is not one plain name (fixed reason).
|
|
3368
|
+
return f"Error: {e}"
|
|
3323
3369
|
except (BulkRNASeqError, ModalityNotFoundError) as e:
|
|
3324
3370
|
logger.error(f"Error in bulk DE direct: {e}")
|
|
3325
3371
|
return f"Error in bulk DE analysis: {str(e)}"
|
|
@@ -3498,7 +3544,7 @@ def de_analysis_expert(
|
|
|
3498
3544
|
- DE results: {de_results_key}
|
|
3499
3545
|
- Ranking metric: {ranking_metric}
|
|
3500
3546
|
- Genes ranked: {len(ranked_df):,}
|
|
3501
|
-
- Databases: {
|
|
3547
|
+
- Databases: {", ".join(db_list)}
|
|
3502
3548
|
- Organism: {organism}
|
|
3503
3549
|
|
|
3504
3550
|
**Results:**
|
|
@@ -3688,12 +3734,19 @@ def de_analysis_expert(
|
|
|
3688
3734
|
targets = [output_path_obj]
|
|
3689
3735
|
if output_format == "xlsx":
|
|
3690
3736
|
targets.append(Path(str(output_path_obj).replace(".xlsx", ".csv")))
|
|
3737
|
+
# Every target stays inside the workspace (or a listed export root).
|
|
3738
|
+
if outside_workspace(data_manager.workspace_path, *targets):
|
|
3739
|
+
return WORKSPACE_EXPORT_MESSAGE
|
|
3691
3740
|
if export_refused(data_manager.workspace_path, *targets):
|
|
3692
3741
|
return MANAGED_EXPORT_MESSAGE
|
|
3693
3742
|
if output_format == "xlsx":
|
|
3694
3743
|
# ...and the name exactly as the late fallback derives it; a path
|
|
3695
3744
|
# that cannot be resolved is refused with the same fixed message.
|
|
3696
3745
|
fallback = _resolved_csv_fallback(output_path_obj)
|
|
3746
|
+
if fallback is not None and outside_workspace(
|
|
3747
|
+
data_manager.workspace_path, fallback
|
|
3748
|
+
):
|
|
3749
|
+
return WORKSPACE_EXPORT_MESSAGE
|
|
3697
3750
|
if fallback is None or export_refused(
|
|
3698
3751
|
data_manager.workspace_path, fallback
|
|
3699
3752
|
):
|
|
@@ -3705,6 +3758,8 @@ def de_analysis_expert(
|
|
|
3705
3758
|
return MANAGED_EXPORT_MESSAGE
|
|
3706
3759
|
output_path_obj.parent.mkdir(parents=True, exist_ok=True)
|
|
3707
3760
|
output_path = str(output_path_obj.resolve())
|
|
3761
|
+
if outside_workspace(data_manager.workspace_path, output_path):
|
|
3762
|
+
return WORKSPACE_EXPORT_MESSAGE
|
|
3708
3763
|
if export_refused(data_manager.workspace_path, *targets, output_path):
|
|
3709
3764
|
return MANAGED_EXPORT_MESSAGE
|
|
3710
3765
|
|
|
@@ -3714,6 +3769,8 @@ def de_analysis_expert(
|
|
|
3714
3769
|
except ImportError:
|
|
3715
3770
|
output_format = "csv"
|
|
3716
3771
|
output_path = output_path.replace(".xlsx", ".csv")
|
|
3772
|
+
if outside_workspace(data_manager.workspace_path, output_path):
|
|
3773
|
+
return WORKSPACE_EXPORT_MESSAGE
|
|
3717
3774
|
if export_refused(data_manager.workspace_path, output_path):
|
|
3718
3775
|
return MANAGED_EXPORT_MESSAGE
|
|
3719
3776
|
results_df.to_csv(output_path, index=True)
|
|
@@ -3748,7 +3805,7 @@ def de_analysis_expert(
|
|
|
3748
3805
|
"results_df = results_df.sort_values('padj')\n"
|
|
3749
3806
|
"results_df.to_{{ output_format }}({{ output_path | string | repr }}, index=True)"
|
|
3750
3807
|
),
|
|
3751
|
-
imports=["pandas"],
|
|
3808
|
+
imports=["import pandas as pd"],
|
|
3752
3809
|
)
|
|
3753
3810
|
|
|
3754
3811
|
data_manager.log_tool_usage(
|
|
@@ -3780,10 +3837,10 @@ def de_analysis_expert(
|
|
|
3780
3837
|
- Format: {output_format.upper()}
|
|
3781
3838
|
- Output: {output_path}
|
|
3782
3839
|
|
|
3783
|
-
**LFC Shrinkage:** {
|
|
3784
|
-
{f
|
|
3840
|
+
**LFC Shrinkage:** {"Applied" if shrinkage_applied else "Not applied"}
|
|
3841
|
+
{f"- Note: {shrinkage_note}" if shrinkage_note else ""}
|
|
3785
3842
|
|
|
3786
|
-
**Publication Columns:** {
|
|
3843
|
+
**Publication Columns:** {", ".join(pub_cols)}
|
|
3787
3844
|
|
|
3788
3845
|
**File ready for publication supplementary tables.**"""
|
|
3789
3846
|
|
|
@@ -144,8 +144,8 @@ Do NOT describe delegation, do NOT ask permission -- execute the tool call.
|
|
|
144
144
|
4. select_variable_features("modality_filtered_normalized") # HVG/deviance selection
|
|
145
145
|
5. run_pca("modality_..._hvg_selected") # PCA reduction
|
|
146
146
|
6. integrate_batches("modality_..._pca", batch_key="...") # If multi-sample
|
|
147
|
-
7. compute_neighbors_and_embed("
|
|
148
|
-
8. cluster_cells("
|
|
147
|
+
7. compute_neighbors_and_embed("chosen_prepared_modality", use_rep="actual_obsm_key") # Use returned representation key
|
|
148
|
+
8. cluster_cells("chosen_prepared_modality", use_rep="actual_obsm_key", batch_correction=False, resolution=0.5) # Reuse; do not rerun upstream
|
|
149
149
|
9. find_marker_genes("modality_clustered", groupby="leiden") # Cluster markers
|
|
150
150
|
10. handoff_to_annotation_expert(...) # If annotation requested
|
|
151
151
|
```
|
|
@@ -153,6 +153,23 @@ Do NOT describe delegation, do NOT ask permission -- execute the tool call.
|
|
|
153
153
|
NOTE: cluster_cells can also perform steps 4-9 automatically as a full pipeline.
|
|
154
154
|
Use composable tools when user asks for specific steps; use cluster_cells for full pipeline.
|
|
155
155
|
|
|
156
|
+
**Prepared clustering contract:** If the caller already prepared PCA, explicitly name
|
|
157
|
+
that modality and use_rep='X_pca'; pass requested n_neighbors/n_pcs. Only the first
|
|
158
|
+
n_pcs components are used. Do not repeat normalization, HVG selection, scaling or PCA.
|
|
159
|
+
A modality name suffix is not evidence that an embedding exists or is scientifically
|
|
160
|
+
fresh: inspect actual obsm keys. Missing/incompatible representations or unhonorable
|
|
161
|
+
explicit dimensions require refusal, not a silent full-pipeline fallback or clamp.
|
|
162
|
+
For **HVG-only** prepared data without PCA, call composable run_pca on the named
|
|
163
|
+
selected modality first, then cluster its returned modality with use_rep='X_pca'.
|
|
164
|
+
Disclose that PCA was computed, not reused. Unset clustering dimensions resolve the
|
|
165
|
+
ordinary 15/30; dimension-driven implicit adjustments must be disclosed. Explicit
|
|
166
|
+
values are never demo-capped. Read actual applied-parameter JSON and recorded IR,
|
|
167
|
+
not schema/request values, when reporting what ran. Never fabricate an applied HVG
|
|
168
|
+
feature count/flavor: full-pipeline exact HVG requests are unsupported; use the
|
|
169
|
+
explicit selection/PCA/reuse workflow. Prepared reuse skips unrequested batch
|
|
170
|
+
correction and says why; explicit batch correction AND a named prepared representation
|
|
171
|
+
refuse together. If correction is needed, do it before preparing/naming the representation.
|
|
172
|
+
|
|
156
173
|
## Bulk Standard Workflow (6 steps)
|
|
157
174
|
|
|
158
175
|
```
|
|
@@ -210,6 +227,36 @@ Report: data_type, n_genes, n_cells/n_samples, mito_pct, n_filtered, cluster_siz
|
|
|
210
227
|
If batch_silhouette > 0.3 or median_lisi < 1.5, flag for re-integration.
|
|
211
228
|
</Response_Format>
|
|
212
229
|
|
|
230
|
+
<Scientific_Reporting_Contract>
|
|
231
|
+
- Report dataset-specific numbers only from tool output for the stated cohort. Never infer
|
|
232
|
+
medians, percentiles, exceedance counts or filtering impact from means/default pass counts.
|
|
233
|
+
- If a requested value is absent, say NOT COMPUTED and offer to compute it; "~" is not evidence.
|
|
234
|
+
- Pass requested thresholds unchanged. Report applied parameters from the tool's actual result,
|
|
235
|
+
not from the user's request, a standard workflow or an earlier modality's configuration.
|
|
236
|
+
- Requested-but-unsupported parameters or extra operations: refuse or explicitly state NOT
|
|
237
|
+
SUPPORTED; never claim they were applied. Exact requests take precedence over default workflows.
|
|
238
|
+
- Report every actually applied filter, including explicit optional-filter disabled states.
|
|
239
|
+
Criterion failures are independent on the original pre-filter cohort: do not sum them.
|
|
240
|
+
Report combined actual removals separately, and distinguish gene counts from observation counts.
|
|
241
|
+
- State the real raw-count storage location returned by the tool. adata.raw is not a counts layer.
|
|
242
|
+
- The supervisor must preserve these numeric/cohort/applied-parameter limitations when reformulating.
|
|
243
|
+
- SC markers: original ranked genes, specificity-filtered candidates and FDR-significant genes
|
|
244
|
+
are different sets. Use the tool's explicit original/filtered/display keys, executed groups
|
|
245
|
+
and aligned counts; never label original rankings as filtered or automatically significant.
|
|
246
|
+
- Disclose actual Scanpy specificity operators/units: positive log2FC > min_fold_change,
|
|
247
|
+
in-group fraction > min_pct, out-group fraction < max_out_pct, compare_abs=False.
|
|
248
|
+
The legacy 1.5 threshold is log2FC, not linear fold-change. No FDR criterion is applied there.
|
|
249
|
+
- Supported adjusted p-values must be interpreted using an explicitly stated criterion.
|
|
250
|
+
Absent statistics are NOT COMPUTED, not zero discoveries; nonfinite values are separate flags.
|
|
251
|
+
Finite extreme abs(log2FC) flags use the tool's diagnostic threshold, without clipping,
|
|
252
|
+
reranking or a significance claim. Near-zero reference expression
|
|
253
|
+
can amplify Scanpy's approximate effect size; an extreme value alone proves neither a
|
|
254
|
+
statistical effect nor a computation error.
|
|
255
|
+
- Cell-level cluster-versus-rest markers are exploratory. Single-donor cells are not independent
|
|
256
|
+
donor replicates and do not establish patient/condition effects; require a biological-unit
|
|
257
|
+
design before such claims. Do not infer uncomputed donor counts from the dataset name.
|
|
258
|
+
</Scientific_Reporting_Contract>
|
|
259
|
+
|
|
213
260
|
<Important_Rules>
|
|
214
261
|
1. **ALWAYS call check_data_status() first** to understand data type and column names
|
|
215
262
|
2. **NEVER assume column names** (leiden, batch, etc.) -- always verify via check_data_status
|
|
@@ -387,6 +434,12 @@ You report results back to the parent agent, not directly to users.
|
|
|
387
434
|
- One donor per condition cannot support a statistical comparison (pseudoreplication): refuse the DE and explain why, even when that donor has several libraries
|
|
388
435
|
- LFC shrinkage is recommended for publication (use extract_and_export_de_results with shrink_lfc=True)
|
|
389
436
|
- GSEA requires sufficient DE genes; run after run_differential_expression, run_de_with_formula, or run_bulk_de_direct
|
|
437
|
+
- Interpretation: ranked, specificity-filtered and FDR-significant genes are distinct sets.
|
|
438
|
+
Name the exact results key and actually applied adjusted-p criterion before claiming
|
|
439
|
+
significance; a large absolute log2FC alone is insufficient. Absent statistics are
|
|
440
|
+
NOT COMPUTED, not zero discoveries; flag nonfinite values separately without changing them.
|
|
441
|
+
- Do not turn exploratory single-donor cell-level cluster markers into patient/condition
|
|
442
|
+
effects. Independent biological units and an appropriate design remain required.
|
|
390
443
|
</Critical_Scientific_Requirements>
|
|
391
444
|
|
|
392
445
|
<Available_Tools>
|