lobster-transcriptomics 1.2.0__tar.gz → 1.2.1__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.
Files changed (30) hide show
  1. {lobster_transcriptomics-1.2.0/lobster_transcriptomics.egg-info → lobster_transcriptomics-1.2.1}/PKG-INFO +1 -1
  2. {lobster_transcriptomics-1.2.0 → lobster_transcriptomics-1.2.1}/lobster/agents/transcriptomics/annotation_expert.py +6 -0
  3. {lobster_transcriptomics-1.2.0 → lobster_transcriptomics-1.2.1}/lobster/agents/transcriptomics/de_analysis_expert.py +67 -14
  4. {lobster_transcriptomics-1.2.0 → lobster_transcriptomics-1.2.1}/lobster/agents/transcriptomics/prompts.py +55 -2
  5. {lobster_transcriptomics-1.2.0 → lobster_transcriptomics-1.2.1}/lobster/agents/transcriptomics/shared_tools.py +152 -38
  6. {lobster_transcriptomics-1.2.0 → lobster_transcriptomics-1.2.1}/lobster/agents/transcriptomics/transcriptomics_expert.py +132 -27
  7. {lobster_transcriptomics-1.2.0 → lobster_transcriptomics-1.2.1}/lobster/services/analysis/enhanced_singlecell_service.py +186 -100
  8. {lobster_transcriptomics-1.2.0 → lobster_transcriptomics-1.2.1}/lobster/services/quality/preprocessing_service.py +54 -17
  9. {lobster_transcriptomics-1.2.0 → lobster_transcriptomics-1.2.1}/lobster/services/quality/quality_service.py +42 -0
  10. {lobster_transcriptomics-1.2.0 → lobster_transcriptomics-1.2.1/lobster_transcriptomics.egg-info}/PKG-INFO +1 -1
  11. {lobster_transcriptomics-1.2.0 → lobster_transcriptomics-1.2.1}/pyproject.toml +1 -1
  12. {lobster_transcriptomics-1.2.0 → lobster_transcriptomics-1.2.1}/LICENSE +0 -0
  13. {lobster_transcriptomics-1.2.0 → lobster_transcriptomics-1.2.1}/README.md +0 -0
  14. {lobster_transcriptomics-1.2.0 → lobster_transcriptomics-1.2.1}/lobster/agents/transcriptomics/__init__.py +0 -0
  15. {lobster_transcriptomics-1.2.0 → lobster_transcriptomics-1.2.1}/lobster/agents/transcriptomics/config.py +0 -0
  16. {lobster_transcriptomics-1.2.0 → lobster_transcriptomics-1.2.1}/lobster/agents/transcriptomics/py.typed +0 -0
  17. {lobster_transcriptomics-1.2.0 → lobster_transcriptomics-1.2.1}/lobster/agents/transcriptomics/state.py +0 -0
  18. {lobster_transcriptomics-1.2.0 → lobster_transcriptomics-1.2.1}/lobster/figure_template_providers/sc.py +0 -0
  19. {lobster_transcriptomics-1.2.0 → lobster_transcriptomics-1.2.1}/lobster/services/analysis/bulk_preprocessing_service.py +0 -0
  20. {lobster_transcriptomics-1.2.0 → lobster_transcriptomics-1.2.1}/lobster/services/analysis/differential_formula_service.py +0 -0
  21. {lobster_transcriptomics-1.2.0 → lobster_transcriptomics-1.2.1}/lobster/services/analysis/pseudobulk_service.py +0 -0
  22. {lobster_transcriptomics-1.2.0 → lobster_transcriptomics-1.2.1}/lobster/services/metadata/manual_annotation_service.py +0 -0
  23. {lobster_transcriptomics-1.2.0 → lobster_transcriptomics-1.2.1}/lobster/services/templates/annotation_templates.py +0 -0
  24. {lobster_transcriptomics-1.2.0 → lobster_transcriptomics-1.2.1}/lobster/services/visualization/bulk_visualization_service.py +0 -0
  25. {lobster_transcriptomics-1.2.0 → lobster_transcriptomics-1.2.1}/lobster_transcriptomics.egg-info/SOURCES.txt +0 -0
  26. {lobster_transcriptomics-1.2.0 → lobster_transcriptomics-1.2.1}/lobster_transcriptomics.egg-info/dependency_links.txt +0 -0
  27. {lobster_transcriptomics-1.2.0 → lobster_transcriptomics-1.2.1}/lobster_transcriptomics.egg-info/entry_points.txt +0 -0
  28. {lobster_transcriptomics-1.2.0 → lobster_transcriptomics-1.2.1}/lobster_transcriptomics.egg-info/requires.txt +0 -0
  29. {lobster_transcriptomics-1.2.0 → lobster_transcriptomics-1.2.1}/lobster_transcriptomics.egg-info/top_level.txt +0 -0
  30. {lobster_transcriptomics-1.2.0 → lobster_transcriptomics-1.2.1}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lobster-transcriptomics
3
- Version: 1.2.0
3
+ Version: 1.2.1
4
4
  Summary: Transcriptomics agents for Lobster AI - single-cell and bulk RNA-seq analysis
5
5
  Author-email: Omics-OS <info@omics-os.com>
6
6
  License-Expression: AGPL-3.0-or-later
@@ -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 (
@@ -1174,21 +1192,26 @@ def de_analysis_expert(
1174
1192
  # Get the modality
1175
1193
  adata = data_manager.get_modality(modality_name)
1176
1194
 
1195
+ # The result name carries the group labels (data values, which may hold
1196
+ # '/'): encoded into one plain name, checked before any analysis.
1197
+ de_modality_name = None
1198
+ if not (_is_pseudobulk(adata) and method == "deseq2"):
1199
+ de_modality_name = _de_result_name(modality_name, group1, group2)
1200
+
1177
1201
  # Every save target of the branch that will run is checked before the
1178
1202
  # data changes or any analysis runs.
1179
1203
  if save_result:
1180
1204
  if _is_pseudobulk(adata) and method == "deseq2":
1181
- assert_export_allowed(
1182
- data_manager.workspace_path,
1183
- data_manager.exports_dir / f"{modality_name}_de_results.csv",
1205
+ results_csv = (
1206
+ data_manager.exports_dir / f"{modality_name}_de_results.csv"
1184
1207
  )
1208
+ assert_inside_workspace(data_manager.workspace_path, results_csv)
1209
+ assert_export_allowed(data_manager.workspace_path, results_csv)
1185
1210
  data_manager._assert_save_allowed(
1186
1211
  f"{modality_name}_with_de_results.h5ad"
1187
1212
  )
1188
1213
  else:
1189
- data_manager._assert_save_allowed(
1190
- f"{modality_name}_de_{group1}_vs_{group2}.h5ad"
1191
- )
1214
+ data_manager._assert_save_allowed(f"{de_modality_name}.h5ad")
1192
1215
 
1193
1216
  logger.info(
1194
1217
  f"Running DE analysis on '{modality_name}': "
@@ -1404,8 +1427,7 @@ def de_analysis_expert(
1404
1427
  de_stats["de_results_key"],
1405
1428
  "adata_de" if internal_method == "deseq2_like" else "adata",
1406
1429
  )
1407
- # Save as new modality
1408
- de_modality_name = f"{modality_name}_de_{group1}_vs_{group2}"
1430
+ # Save as new modality (name derived and checked before the analysis)
1409
1431
  data_manager.store_modality(
1410
1432
  name=de_modality_name,
1411
1433
  adata=adata_de,
@@ -1414,7 +1436,7 @@ def de_analysis_expert(
1414
1436
  )
1415
1437
 
1416
1438
  if save_result:
1417
- save_path = f"{modality_name}_de_{group1}_vs_{group2}.h5ad"
1439
+ save_path = f"{de_modality_name}.h5ad"
1418
1440
  data_manager.save_modality(de_modality_name, save_path)
1419
1441
 
1420
1442
  data_manager.log_tool_usage(
@@ -1471,6 +1493,12 @@ def de_analysis_expert(
1471
1493
  analysis_results["details"]["differential_expression"] = response
1472
1494
  return response
1473
1495
 
1496
+ except WorkspaceExportError:
1497
+ # A save target is outside the workspace: nothing was saved.
1498
+ return WORKSPACE_EXPORT_MESSAGE
1499
+ except InvalidModalityNameError as e:
1500
+ # The derived result name is not one plain name (fixed reason).
1501
+ return f"Error: {e}"
1474
1502
  except ManagedPathError:
1475
1503
  # A save target is inside the managed figure folder: nothing was saved.
1476
1504
  return MANAGED_EXPORT_MESSAGE
@@ -1989,11 +2017,12 @@ def de_analysis_expert(
1989
2017
 
1990
2018
  # Both save targets are checked before any analysis or store.
1991
2019
  if save_results:
1992
- assert_export_allowed(
1993
- data_manager.workspace_path,
2020
+ formula_csv = (
1994
2021
  data_manager.exports_dir
1995
- / f"{pseudobulk_modality}_formula_de_results.csv",
2022
+ / f"{pseudobulk_modality}_formula_de_results.csv"
1996
2023
  )
2024
+ assert_inside_workspace(data_manager.workspace_path, formula_csv)
2025
+ assert_export_allowed(data_manager.workspace_path, formula_csv)
1997
2026
  data_manager._assert_save_allowed(
1998
2027
  f"{pseudobulk_modality}_with_formula_results.h5ad"
1999
2028
  )
@@ -2192,6 +2221,9 @@ def de_analysis_expert(
2192
2221
 
2193
2222
  return response
2194
2223
 
2224
+ except WorkspaceExportError:
2225
+ # A save target is outside the workspace: nothing was saved.
2226
+ return WORKSPACE_EXPORT_MESSAGE
2195
2227
  except ManagedPathError:
2196
2228
  # A save target is inside the managed figure folder: nothing was saved.
2197
2229
  return MANAGED_EXPORT_MESSAGE
@@ -3034,6 +3066,9 @@ def de_analysis_expert(
3034
3066
  output_path_obj = Path(output_path)
3035
3067
  if not output_path_obj.is_absolute():
3036
3068
  output_path_obj = data_manager.exports_dir / output_path_obj
3069
+ # The destination stays inside the workspace (or a listed export root).
3070
+ if outside_workspace(data_manager.workspace_path, output_path_obj):
3071
+ return WORKSPACE_EXPORT_MESSAGE
3037
3072
  # Figures are saved only through the trusted save step: check the
3038
3073
  # caller's (unresolved) destination, and every folder its recursive
3039
3074
  # creation can make, before anything is created.
@@ -3045,6 +3080,8 @@ def de_analysis_expert(
3045
3080
  return MANAGED_EXPORT_MESSAGE
3046
3081
  output_path_obj.parent.mkdir(parents=True, exist_ok=True)
3047
3082
  output_path = str(output_path_obj.resolve())
3083
+ if outside_workspace(data_manager.workspace_path, output_path):
3084
+ return WORKSPACE_EXPORT_MESSAGE
3048
3085
  if export_refused(
3049
3086
  data_manager.workspace_path, output_path_obj, output_path
3050
3087
  ):
@@ -3189,6 +3226,9 @@ def de_analysis_expert(
3189
3226
  if group2 not in available_groups_str:
3190
3227
  return f"Group '{group2}' not found in '{group_key}'. Available: {available_groups}"
3191
3228
 
3229
+ # The result name carries the group labels: encoded and checked first.
3230
+ de_modality_name = _de_result_name(modality_name, group1, group2)
3231
+
3192
3232
  # Ensure string type matching
3193
3233
  if group1 not in available_groups:
3194
3234
  adata.obs[group_key] = adata.obs[group_key].astype(str)
@@ -3223,8 +3263,7 @@ def de_analysis_expert(
3223
3263
  if refusal:
3224
3264
  return refusal
3225
3265
  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}"
3266
+ # Store result modality (name derived and checked before the analysis)
3228
3267
  data_manager.store_modality(
3229
3268
  name=de_modality_name,
3230
3269
  adata=adata_de,
@@ -3320,6 +3359,9 @@ def de_analysis_expert(
3320
3359
 
3321
3360
  return response
3322
3361
 
3362
+ except InvalidModalityNameError as e:
3363
+ # The derived result name is not one plain name (fixed reason).
3364
+ return f"Error: {e}"
3323
3365
  except (BulkRNASeqError, ModalityNotFoundError) as e:
3324
3366
  logger.error(f"Error in bulk DE direct: {e}")
3325
3367
  return f"Error in bulk DE analysis: {str(e)}"
@@ -3688,12 +3730,19 @@ def de_analysis_expert(
3688
3730
  targets = [output_path_obj]
3689
3731
  if output_format == "xlsx":
3690
3732
  targets.append(Path(str(output_path_obj).replace(".xlsx", ".csv")))
3733
+ # Every target stays inside the workspace (or a listed export root).
3734
+ if outside_workspace(data_manager.workspace_path, *targets):
3735
+ return WORKSPACE_EXPORT_MESSAGE
3691
3736
  if export_refused(data_manager.workspace_path, *targets):
3692
3737
  return MANAGED_EXPORT_MESSAGE
3693
3738
  if output_format == "xlsx":
3694
3739
  # ...and the name exactly as the late fallback derives it; a path
3695
3740
  # that cannot be resolved is refused with the same fixed message.
3696
3741
  fallback = _resolved_csv_fallback(output_path_obj)
3742
+ if fallback is not None and outside_workspace(
3743
+ data_manager.workspace_path, fallback
3744
+ ):
3745
+ return WORKSPACE_EXPORT_MESSAGE
3697
3746
  if fallback is None or export_refused(
3698
3747
  data_manager.workspace_path, fallback
3699
3748
  ):
@@ -3705,6 +3754,8 @@ def de_analysis_expert(
3705
3754
  return MANAGED_EXPORT_MESSAGE
3706
3755
  output_path_obj.parent.mkdir(parents=True, exist_ok=True)
3707
3756
  output_path = str(output_path_obj.resolve())
3757
+ if outside_workspace(data_manager.workspace_path, output_path):
3758
+ return WORKSPACE_EXPORT_MESSAGE
3708
3759
  if export_refused(data_manager.workspace_path, *targets, output_path):
3709
3760
  return MANAGED_EXPORT_MESSAGE
3710
3761
 
@@ -3714,6 +3765,8 @@ def de_analysis_expert(
3714
3765
  except ImportError:
3715
3766
  output_format = "csv"
3716
3767
  output_path = output_path.replace(".xlsx", ".csv")
3768
+ if outside_workspace(data_manager.workspace_path, output_path):
3769
+ return WORKSPACE_EXPORT_MESSAGE
3717
3770
  if export_refused(data_manager.workspace_path, output_path):
3718
3771
  return MANAGED_EXPORT_MESSAGE
3719
3772
  results_df.to_csv(output_path, index=True)
@@ -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("modality_..._integrated") # Neighbor graph + UMAP
148
- 8. cluster_cells("modality_..._embedded", resolution=0.5) # Leiden clustering (or algorithm="louvain" if specified)
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>