From f89562585ac61ff1b99b68d5220c2eeeb776b499 Mon Sep 17 00:00:00 2001 From: subindevs <36504048+subindevs@users.noreply.github.com> Date: Sun, 14 Jun 2026 22:12:04 +0100 Subject: [PATCH 1/8] Add incremental mypy config, CI, and pre-commit wiring (#46) Adds a lenient [tool.mypy] config with an explicit ignore_errors override list for the 113 modules that don't yet pass, runs mypy in the lint CI job and as a pre-commit hook, and adds mypy to the dev dependency group. New code is held to the standard; the override list shrinks as modules are cleaned up. Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/lint.yml | 6 ++ .pre-commit-config.yaml | 7 ++ pyproject.toml | 129 +++++++++++++++++++++++++++++++++++++ 3 files changed, 142 insertions(+) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 5a92ceac..58d2538e 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -24,3 +24,9 @@ jobs: - name: ruff format check run: ruff format --check . + + - name: Install mypy + run: pip install mypy + + - name: mypy + run: mypy . diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7b184093..7f6f8bdf 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -5,3 +5,10 @@ repos: - id: ruff args: [--fix] - id: ruff-format + + - repo: https://github.com/pre-commit/mirrors-mypy + rev: v2.1.0 # run `pre-commit autoupdate` to pin to latest + hooks: + - id: mypy + pass_filenames: false + args: ["."] diff --git a/pyproject.toml b/pyproject.toml index 003ae9e3..82cc2dcf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -94,6 +94,7 @@ dev = [ "pytest-asyncio>=0.21.0", "ruff>=0.4.0", "pre-commit>=3.7.0", + "mypy>=1.8.0", ] [project.scripts] @@ -145,3 +146,131 @@ target-version = "py310" [tool.ruff.lint] select = ["E", "F", "I", "UP", "B"] + +[tool.mypy] +python_version = "3.10" +ignore_missing_imports = true +explicit_package_bases = true +mypy_path = "." + +# Modules with pre-existing mypy errors, exempted while the codebase is +# incrementally annotated (see issue #46). New modules should not be added +# here; PRs that substantively touch a listed module should fix its errors +# and remove it from this list as part of that change. +[[tool.mypy.overrides]] +module = [ + "benchmarks.perception.live_viewer", + "benchmarks.perception.metrics", + "benchmarks.perception.testset", + "diagnostics.benchmark_gentlystore_fps", + "diagnostics.benchmark_volume_fps", + "diagnostics.plot_benchmark_results", + "diagnostics.switchbot_webgui", + "examples.example_dispim_workflows", + "gently", + "gently.agent", + "gently.analysis.core", + "gently.analysis.steps", + "gently.app.agent", + "gently.app.benchmark", + "gently.app.calibration.edge_roi", + "gently.app.detectors.dopaminergic_signal", + "gently.app.orchestration.timelapse", + "gently.app.tools.acquisition_tools", + "gently.app.tools.analysis_tools", + "gently.app.tools.calibration_tools", + "gently.app.tools.detection_tools", + "gently.app.tools.experiment_tools", + "gently.app.tools.focus_tools", + "gently.app.tools.interaction_tools", + "gently.app.tools.led_tools", + "gently.app.tools.light_source_tools", + "gently.app.tools.memory_tools", + "gently.app.tools.plan_execution_tools", + "gently.app.tools.resolution_tools", + "gently.app.tools.session_tools", + "gently.app.tools.stage_tools", + "gently.app.tools.temperature_tools", + "gently.app.tools.timelapse_tools", + "gently.app.tools.volume_tools", + "gently.app.video_maker", + "gently.core.coordinates", + "gently.core.event_bus", + "gently.core.file_store", + "gently.core.imaging", + "gently.core.service", + "gently.core.store", + "gently.dataset.aggregator", + "gently.dataset.embryo_dataset", + "gently.dataset.explorer_server", + "gently.detection", + "gently.eval.decision_log", + "gently.eval.event_capture", + "gently.gently", + "gently.hardware.dispim.client", + "gently.hardware.dispim.device_factory", + "gently.hardware.dispim.device_layer", + "gently.hardware.dispim.devices.acquisition", + "gently.hardware.dispim.devices.camera", + "gently.hardware.dispim.devices.optical", + "gently.hardware.dispim.devices.scanner", + "gently.hardware.dispim.devices.test_temperature_controller", + "gently.hardware.dispim.plans.acquisition", + "gently.hardware.dispim.sam_detection", + "gently.hardware.switchbot", + "gently.harness.conversation", + "gently.harness.detection.detector", + "gently.harness.detection.queue", + "gently.harness.detection.verifier", + "gently.harness.memory", + "gently.harness.memory.file_store", + "gently.harness.memory.gap_assessment", + "gently.harness.memory._intentions", + "gently.harness.memory.interface", + "gently.harness.memory._ml_pipelines", + "gently.harness.memory.onboarding", + "gently.harness.memory._plans", + "gently.harness.memory.startup_wizard", + "gently.harness.memory._understanding", + "gently.harness.microscope", + "gently.harness.plan_mode.tools.lab_context", + "gently.harness.plan_mode.tools.planning", + "gently.harness.plan_mode.tools.research", + "gently.harness.plan_mode.tools.templates", + "gently.harness.plan_mode.tools.validation", + "gently.harness.prompts.manager", + "gently.harness.prompts.templates", + "gently.harness.session.interaction_logger", + "gently.harness.session.manager", + "gently.harness.session.timeline", + "gently.harness.state", + "gently.harness.tools.registry", + "gently.log_config", + "gently.mesh.capability_provider", + "gently.mesh.discovery", + "gently.mesh.peer_client", + "gently.mesh.verse_map", + "gently.ml.data_loader", + "gently.ml.federated", + "gently.organisms.celegans.developmental_tracker", + "gently.ui.web.connection_manager", + "gently.ui.web.routes.agent_ws", + "gently.ui.web.routes.campaigns", + "gently.ui.web.routes.data", + "gently.ui.web.routes.volumes", + "gently.ui.web.routes.websocket", + "gently.ui.web.server", + "gently.ui.web.strategy_snapshot", + "launch_gently", + "scripts.auto_label_examples", + "scripts.extract_stage_examples", + "scripts.gemini_stage_test", + "scripts.launch_viz_server", + "scripts.projection_explorer", + "scripts.replay_session", + "scripts.stage_annotator", + "tests.test_campaign_coordination", + "tests.test_text_tool_call_extraction", + "tests.validate_recursive_autofocus", +] +ignore_errors = true From baeaa4b9fd8080c753463d733a5ae250c78b2f0e Mon Sep 17 00:00:00 2001 From: subindevs <36504048+subindevs@users.noreply.github.com> Date: Sun, 14 Jun 2026 22:12:11 +0100 Subject: [PATCH 2/8] Document incremental mypy typing policy in CONTRIBUTING.md Co-Authored-By: Claude Sonnet 4.6 --- CONTRIBUTING.md | 35 ++++++++++++++++++++++++++++------- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7e75550a..b936732d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,14 +2,13 @@ ## Code quality toolchain -This project uses [ruff](https://docs.astral.sh/ruff/) for linting and formatting, and [pre-commit](https://pre-commit.com/) to enforce this automatically before every commit. - -> **Note:** Type checking with mypy is not yet enforced. See the tracking issue for gradually -> introducing mypy across the codebase. +This project uses [ruff](https://docs.astral.sh/ruff/) for linting and formatting, and +[mypy](https://mypy-lang.org/) for type checking, enforced automatically before every +commit via [pre-commit](https://pre-commit.com/). ### First-time setup -Install the dev dependencies (includes ruff and pre-commit): +Install the dev dependencies (includes ruff, mypy, and pre-commit): ```bash uv sync @@ -21,7 +20,8 @@ Then install the pre-commit hooks: pre-commit install ``` -From this point on, ruff runs automatically on staged files whenever you `git commit`. +From this point on, ruff runs on staged files and mypy runs across the whole +project whenever you `git commit`. ### Running manually @@ -46,6 +46,27 @@ To update hook versions to their latest releases: pre-commit autoupdate ``` +### Type checking + +Run mypy the same way pre-commit and CI do: + +```bash +mypy . +``` + +The codebase is being typed incrementally (see issue #46). Modules with +pre-existing errors are listed in the `[[tool.mypy.overrides]]` block in +`pyproject.toml` with `ignore_errors = true`, so `mypy .` passes today even +though not every module is fully typed yet. + +Policy for working with this list: + +- **New modules** must pass `mypy .` cleanly — do not add them to the + overrides list. +- **PRs that substantively touch a module on the overrides list** should fix + that module's type errors and remove it from the list as part of the + change. + ### CI -Every pull request runs the lint job (`.github/workflows/lint.yml`), which checks ruff lint and formatting across the entire project. Fix any failures locally with `pre-commit run --all-files` before pushing. +Every pull request runs the lint job (`.github/workflows/lint.yml`), which checks ruff lint and formatting and runs mypy across the entire project. Fix any failures locally with `pre-commit run --all-files` before pushing. From 0b4927f4dcbfafd861ddbe7392ee5731e0e00237 Mon Sep 17 00:00:00 2001 From: subindevs <36504048+subindevs@users.noreply.github.com> Date: Sun, 14 Jun 2026 22:12:19 +0100 Subject: [PATCH 3/8] Fix mypy errors in gently/harness/bridge.py Annotates two implicit-Optional defaults and fixes a str/Path arg-type mismatch in create_timelapse_video. Removes the file from the mypy override list before it's even added. Co-Authored-By: Claude Sonnet 4.6 --- gently/harness/bridge.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gently/harness/bridge.py b/gently/harness/bridge.py index 400cfc60..ddbfa9ca 100644 --- a/gently/harness/bridge.py +++ b/gently/harness/bridge.py @@ -718,7 +718,7 @@ async def handle_command( self, command: str, send_fn: Callable[[dict], Coroutine], - choice_futures: dict = None, + choice_futures: dict | None = None, ) -> None: """ Execute a slash command and send the result. @@ -1413,7 +1413,7 @@ async def handle_command( lines = [f"Creating timelapse videos (fps={fps})..."] for eid, vol_paths in all_volumes.items(): output_path = session_images_dir / f"{eid}_timelapse.mp4" - create_timelapse_video(vol_paths, str(output_path), fps=fps) + create_timelapse_video(vol_paths, output_path, fps=fps) lines.append(f" {eid}: {len(vol_paths)} frames → {output_path.name}") await send_fn( @@ -1654,7 +1654,7 @@ def _get_status_data(self) -> dict: "has_sam": client.has_sam if client else False, } - def _get_embryos_data(self, embryo_id: str = None) -> dict: + def _get_embryos_data(self, embryo_id: str | None = None) -> dict: """Build structured embryo data.""" exp = self.agent.experiment if embryo_id: From b4dc3b7fc6faea86a40c81edc005d1c8b3f8c6bf Mon Sep 17 00:00:00 2001 From: subindevs <36504048+subindevs@users.noreply.github.com> Date: Sun, 14 Jun 2026 23:00:44 +0100 Subject: [PATCH 4/8] Fix implicit-Optional parameter defaults across 45 modules Changes def f(x: T = None) to def f(x: T | None = None) to match PEP 484 / mypy's no_implicit_optional, matching the pattern already applied to bridge.py in #42. Removes 14 modules from the mypy ignore_errors override list (113 -> 99) that are now fully clean. --- gently/app/agent.py | 10 +- gently/app/benchmark.py | 6 +- gently/app/orchestration/timelapse.py | 8 +- gently/app/tools/acquisition_tools.py | 12 +- gently/app/tools/analysis_tools.py | 4 +- gently/app/tools/calibration_tools.py | 12 +- gently/app/tools/detection_tools.py | 14 +-- gently/app/tools/focus_tools.py | 8 +- gently/app/tools/interaction_tools.py | 2 +- gently/app/tools/light_source_tools.py | 4 +- gently/app/tools/memory_tools.py | 16 +-- gently/app/tools/plan_execution_tools.py | 8 +- gently/app/tools/resolution_tools.py | 18 +-- gently/app/tools/session_tools.py | 16 +-- gently/app/tools/stage_tools.py | 2 +- gently/app/tools/timelapse_tools.py | 76 +++++++------ gently/app/tools/volume_tools.py | 14 +-- gently/app/video_maker.py | 2 +- gently/core/coordinates.py | 6 +- gently/core/file_store.py | 64 +++++------ gently/core/store.py | 58 +++++----- gently/dataset/explorer_server.py | 4 +- gently/hardware/dispim/client.py | 42 +++---- gently/hardware/dispim/device_layer.py | 4 +- gently/hardware/dispim/devices/acquisition.py | 10 +- gently/hardware/dispim/devices/optical.py | 4 +- gently/hardware/dispim/devices/scanner.py | 2 +- gently/hardware/dispim/plans/acquisition.py | 16 +-- gently/harness/memory/interface.py | 12 +- gently/harness/microscope.py | 2 +- gently/harness/plan_mode/tools/lab_context.py | 4 +- gently/harness/plan_mode/tools/planning.py | 106 +++++++++--------- gently/harness/plan_mode/tools/research.py | 6 +- gently/harness/plan_mode/tools/templates.py | 10 +- gently/harness/plan_mode/tools/validation.py | 2 +- gently/harness/prompts/manager.py | 2 +- gently/harness/prompts/templates.py | 6 +- gently/harness/state.py | 16 +-- gently/harness/tools/registry.py | 2 +- gently/log_config.py | 4 +- gently/mesh/verse_map.py | 2 +- gently/ui/web/connection_manager.py | 6 +- gently/ui/web/server.py | 6 +- launch_gently.py | 2 +- pyproject.toml | 14 --- scripts/gemini_stage_test.py | 6 +- 46 files changed, 327 insertions(+), 323 deletions(-) diff --git a/gently/app/agent.py b/gently/app/agent.py index 5122e80f..4a5f6602 100644 --- a/gently/app/agent.py +++ b/gently/app/agent.py @@ -79,7 +79,7 @@ def __init__( model: str = settings.models.main, microscope_client=None, session_id: str | None = None, - store: FileStore = None, + store: FileStore | None = None, no_api: bool = False, ): """ @@ -411,7 +411,7 @@ def enter_resolution_mode(self) -> str: logger.info("Entered resolution mode") return "Resolution mode active. Determining what this session is for." - def exit_resolution_mode(self, outcome: str = None) -> str: + def exit_resolution_mode(self, outcome: str | None = None) -> str: """Leave resolution mode for run mode. Called by resolution tools (attach_session_to_plan, @@ -490,7 +490,7 @@ def exit_plan_mode(self) -> str: # ===== Prompt & System Prompt ===== - def _update_system_prompt(self, context_summary: str = None): + def _update_system_prompt(self, context_summary: str | None = None): """Rebuild system prompt via PromptManager.""" self.system_prompt = self.prompts.update_system_prompt( self.experiment, @@ -1065,7 +1065,9 @@ async def handle_message_stream(self, user_message: str): if acquired: lock.release() - async def run_wake_turn(self, wake_note: str, trigger: str = None, interactive: bool = False): + async def run_wake_turn( + self, wake_note: str, trigger: str | None = None, interactive: bool = False + ): """Drive one autonomous (no-user) turn for the wake-router. Runs through the normal streaming pipeline (so it acquires the turn-lock diff --git a/gently/app/benchmark.py b/gently/app/benchmark.py index 643e296f..9263d7fe 100644 --- a/gently/app/benchmark.py +++ b/gently/app/benchmark.py @@ -138,9 +138,9 @@ async def run_benchmark( exposure_ms: float = 10.0, warmup: int = 1, # Legacy parameters (ignored, kept for API compat with bridge) - n_volumes: int = None, - n_slices: int = None, - n_warmup: int = None, + n_volumes: int | None = None, + n_slices: int | None = None, + n_warmup: int | None = None, progress_fn: callable | None = None, ) -> BenchmarkResults: """ diff --git a/gently/app/orchestration/timelapse.py b/gently/app/orchestration/timelapse.py index 5a22eea0..5d0783e4 100644 --- a/gently/app/orchestration/timelapse.py +++ b/gently/app/orchestration/timelapse.py @@ -739,7 +739,7 @@ async def _run_loop(self): }, ) - async def _acquire_embryo(self, embryo_state: EmbryoState, round_time: datetime = None): + async def _acquire_embryo(self, embryo_state: EmbryoState, round_time: datetime | None = None): """Acquire a single volume for one embryo Parameters @@ -2510,7 +2510,7 @@ def _check_interval_rules( result, ) - def _finalize_perception_run(self, status: str = "completed", error_message: str = None): + def _finalize_perception_run(self, status: str = "completed", error_message: str | None = None): """Mark the perception run as finished in FileStore.""" if self._store and self._perception_run_id is not None: try: @@ -2575,7 +2575,7 @@ async def _run_detector( volume, embryo_state: EmbryoState, detector_name: str, - volume_uids: dict = None, + volume_uids: dict | None = None, ): """Run a role-declared Detector (Phase 2) and persist + emit results. @@ -2791,7 +2791,7 @@ async def _run_perception( timepoint: int, volume, embryo_state: EmbryoState, - volume_uids: dict = None, + volume_uids: dict | None = None, ): """Run the per-role detector on the acquired volume and emit results. diff --git a/gently/app/tools/acquisition_tools.py b/gently/app/tools/acquisition_tools.py index d73046c0..83a2deae 100644 --- a/gently/app/tools/acquisition_tools.py +++ b/gently/app/tools/acquisition_tools.py @@ -44,8 +44,8 @@ async def acquire_volume( embryo_id: str, num_slices: int = 50, exposure_ms: float = 10.0, - z_buffer_um: float = None, - context: dict = None, + z_buffer_um: float | None = None, + context: dict | None = None, ) -> str: """Acquire single volume - moves to embryo first, uses calibration""" agent = context.get("agent") @@ -237,11 +237,11 @@ async def acquire_volume( ], ) async def capture_lightsheet( - piezo_position: float = None, + piezo_position: float | None = None, galvo_position: float = 0.0, - embryo_id: str = None, + embryo_id: str | None = None, show: bool = True, - context: dict = None, + context: dict | None = None, ) -> str: """Capture and optionally display a single lightsheet image""" client = context.get("client") @@ -355,7 +355,7 @@ async def capture_lightsheet( ToolExample("Capture all embryos", {}), ], ) -async def batch_lightsheet(galvo_position: float = 0.0, context: dict = None) -> str: +async def batch_lightsheet(galvo_position: float = 0.0, context: dict | None = None) -> str: """Capture lightsheet images from all embryos and show them in the web UI""" agent = context.get("agent") client = context.get("client") diff --git a/gently/app/tools/analysis_tools.py b/gently/app/tools/analysis_tools.py index 6635d9cb..1e897b61 100644 --- a/gently/app/tools/analysis_tools.py +++ b/gently/app/tools/analysis_tools.py @@ -18,7 +18,7 @@ async def analyze_volume( analysis_prompt: str, use_recent_context: bool = False, timepoint: int | None = None, - context: dict = None, + context: dict | None = None, ) -> str: """Analyze embryo volume with Claude Vision""" agent, err = require_agent(context) @@ -60,7 +60,7 @@ async def analyze_volume( def get_recent_perceptions( embryo_id: str | None = None, n: int = 5, - context: dict = None, + context: dict | None = None, ) -> str: """Read live per-embryo perception state from the perception sessions. diff --git a/gently/app/tools/calibration_tools.py b/gently/app/tools/calibration_tools.py index 2220e375..5769cee1 100644 --- a/gently/app/tools/calibration_tools.py +++ b/gently/app/tools/calibration_tools.py @@ -946,15 +946,15 @@ async def fast_calibrate_embryo( async def calibrate_embryo( embryo_id: str, skip_edge_detection: bool = False, - galvo_top: float = None, - galvo_bottom: float = None, + galvo_top: float | None = None, + galvo_bottom: float | None = None, edge_step: float = 0.05, edge_max_range: float = 0.5, edge_tolerance_deg: float = 0.20, inset_fraction: float = 0.4, z_buffer_um: float = 25.0, use_v04_plan: bool = False, - context: dict = None, + context: dict | None = None, ) -> str: """Run piezo-galvo calibration with Claude vision edge detection. @@ -1396,10 +1396,10 @@ async def check_embryo_at_position(galvo_pos: float) -> bool: ], ) async def calibrate_all_embryos( - embryo_ids: list[str] = None, + embryo_ids: list[str] | None = None, skip_edge_detection: bool = False, z_buffer_um: float = 25.0, - context: dict = None, + context: dict | None = None, ) -> str: """Calibrate all embryos sequentially with Claude vision""" agent = context.get("agent") @@ -1512,7 +1512,7 @@ def apply_calibration_to_embryos( source_embryo_id: str, target_embryo_ids: list[str] | None = None, overwrite_existing: bool = True, - context: dict = None, + context: dict | None = None, ) -> str: """Broadcast one embryo's calibration to others. diff --git a/gently/app/tools/detection_tools.py b/gently/app/tools/detection_tools.py index 89d914d6..b821ec72 100644 --- a/gently/app/tools/detection_tools.py +++ b/gently/app/tools/detection_tools.py @@ -113,12 +113,12 @@ async def detect_embryos( auto_calibrate: bool = False, min_confidence: float = 0.7, use_claude_review: bool = False, - exposure_ms: float = None, + exposure_ms: float | None = None, brightness_percentile: float = 99.0, min_area: int = 5000, max_area: int = 150000, default_role: str = "test", - context: dict = None, + context: dict | None = None, ) -> str: """Detect embryos via SAM + edit/assign roles in the web map view.""" agent = context.get("agent") @@ -276,9 +276,9 @@ async def detect_embryos( ], ) async def manual_mark_embryos( - exposure_ms: float = None, + exposure_ms: float | None = None, default_role: str = "test", - context: dict = None, + context: dict | None = None, ) -> str: """Manual marking via the web map view.""" agent = context.get("agent") @@ -415,9 +415,9 @@ async def manual_mark_embryos( ], ) async def edit_embryos( - exposure_ms: float = None, + exposure_ms: float | None = None, default_role: str = "test", - context: dict = None, + context: dict | None = None, ) -> str: """Edit existing embryos via the web map view.""" agent = context.get("agent") @@ -450,7 +450,7 @@ async def edit_embryos( ToolExample("Display embryo positions", {}), ], ) -async def show_detected_embryos(save_to_file: bool = True, context: dict = None) -> str: +async def show_detected_embryos(save_to_file: bool = True, context: dict | None = None) -> str: """Show detected embryos visualization using experiment.embryos as source of truth""" agent = context.get("agent") client = context.get("client") diff --git a/gently/app/tools/focus_tools.py b/gently/app/tools/focus_tools.py index 63f043e5..cef6ca81 100644 --- a/gently/app/tools/focus_tools.py +++ b/gently/app/tools/focus_tools.py @@ -57,7 +57,7 @@ async def fine_focus( move_to_best: bool = True, galvo_position: float = 0.0, embryo_id: str | None = None, - context: dict = None, + context: dict | None = None, ) -> str: """ Perform fine focus sweep to find optimal piezo position. @@ -234,10 +234,10 @@ async def fine_focus( ], ) async def get_focus_score( - piezo_position: float = None, + piezo_position: float | None = None, galvo_position: float = 0.0, algorithm: str = "fft_bandpass", - context: dict = None, + context: dict | None = None, ) -> str: """ Get focus score for current or specified position. @@ -304,7 +304,7 @@ async def get_focus_score( ToolExample("Check focus history for embryo 2", {"embryo_id": "embryo_2"}), ], ) -async def get_focus_history(embryo_id: str, context: dict = None) -> str: +async def get_focus_history(embryo_id: str, context: dict | None = None) -> str: """ Get focus measurement history for an embryo. diff --git a/gently/app/tools/interaction_tools.py b/gently/app/tools/interaction_tools.py index 3e667823..cf3f6f45 100644 --- a/gently/app/tools/interaction_tools.py +++ b/gently/app/tools/interaction_tools.py @@ -58,7 +58,7 @@ async def ask_user_choice( options: list[dict[str, str]], allow_multiple: bool = False, default_id: str | None = None, - context: dict = None, + context: dict | None = None, ) -> str: """ Present user with selectable options. diff --git a/gently/app/tools/light_source_tools.py b/gently/app/tools/light_source_tools.py index 0f144082..065b981f 100644 --- a/gently/app/tools/light_source_tools.py +++ b/gently/app/tools/light_source_tools.py @@ -43,7 +43,7 @@ async def set_laser_power( wavelength: int, pct: float, - context: dict = None, + context: dict | None = None, ) -> str: """Set laser power and read back the actual setpoint.""" agent, err = require_agent(context) @@ -87,7 +87,7 @@ async def set_laser_power( ) async def get_laser_power( wavelength: int, - context: dict = None, + context: dict | None = None, ) -> str: """Read current laser power %.""" agent, err = require_agent(context) diff --git a/gently/app/tools/memory_tools.py b/gently/app/tools/memory_tools.py index f586d087..0c0306e2 100644 --- a/gently/app/tools/memory_tools.py +++ b/gently/app/tools/memory_tools.py @@ -36,7 +36,7 @@ def _get_memory(context: dict): ) async def recall_campaigns( status: str = "active", - context: dict = None, + context: dict | None = None, ) -> str: """List campaigns filtered by status.""" memory = _get_memory(context) @@ -64,9 +64,9 @@ async def recall_campaigns( ], ) async def recall_learnings( - query: str = None, + query: str | None = None, limit: int = 20, - context: dict = None, + context: dict | None = None, ) -> str: """Search or list learnings.""" memory = _get_memory(context) @@ -93,10 +93,10 @@ async def recall_learnings( ], ) async def recall_observations( - query: str = None, - embryo_id: str = None, + query: str | None = None, + embryo_id: str | None = None, limit: int = 20, - context: dict = None, + context: dict | None = None, ) -> str: """Search or list observations.""" memory = _get_memory(context) @@ -121,8 +121,8 @@ async def recall_observations( ], ) async def recall_context( - campaign_id: str = None, - context: dict = None, + campaign_id: str | None = None, + context: dict | None = None, ) -> str: """Full context snapshot.""" memory = _get_memory(context) diff --git a/gently/app/tools/plan_execution_tools.py b/gently/app/tools/plan_execution_tools.py index 38c1cc31..f07e8af0 100644 --- a/gently/app/tools/plan_execution_tools.py +++ b/gently/app/tools/plan_execution_tools.py @@ -40,8 +40,8 @@ ) async def execute_plan_item( item_ref: str, - embryo_ids: list[str] = None, - context: dict = None, + embryo_ids: list[str] | None = None, + context: dict | None = None, ) -> str: """Execute a planned imaging item.""" agent, err = require_agent(context) @@ -198,8 +198,8 @@ async def execute_plan_item( ) async def complete_current_plan_item( item_ref: str, - outcome: str = None, - context: dict = None, + outcome: str | None = None, + context: dict | None = None, ) -> str: """Complete a plan item and report newly unblocked items.""" agent, err = require_agent(context) diff --git a/gently/app/tools/resolution_tools.py b/gently/app/tools/resolution_tools.py index 5de2a5bd..f5a6c19f 100644 --- a/gently/app/tools/resolution_tools.py +++ b/gently/app/tools/resolution_tools.py @@ -92,7 +92,7 @@ def _short(text: str | None, n: int = 80) -> str: async def attach_session_to_plan( plan_item_id: str, rationale: str = "", - context: dict = None, + context: dict | None = None, ) -> str: """Attach the current session to a plan item.""" agent, err = require_agent(context) @@ -160,7 +160,7 @@ async def attach_session_to_plan( ) async def mark_session_standalone( description: str, - context: dict = None, + context: dict | None = None, ) -> str: """Mark the session as a standalone (non-plan) run.""" agent, err = require_agent(context) @@ -212,7 +212,7 @@ async def mark_session_standalone( ) async def detach_session_from_plan( reason: str = "", - context: dict = None, + context: dict | None = None, ) -> str: """Detach the current session from its plan item.""" agent, err = require_agent(context) @@ -276,7 +276,7 @@ async def mark_plan_item_status( plan_item_id: str, status: str, notes: str = "", - context: dict = None, + context: dict | None = None, ) -> str: """Update a plan item's status.""" agent, err = require_agent(context) @@ -359,8 +359,8 @@ def _apply_spec_to_embryo(embryo, spec) -> list[str]: ) async def apply_plan_acquisition_spec( plan_item_id: str, - overrides: dict = None, - context: dict = None, + overrides: dict | None = None, + context: dict | None = None, ) -> str: """Apply a plan's ImagingSpec to the experiment.""" agent, err = require_agent(context) @@ -512,7 +512,7 @@ class _Filtered: async def recall_sibling_sessions( identifier: str, limit: int = 10, - context: dict = None, + context: dict | None = None, ) -> str: """Return sessions sharing the given plan item's campaign or the campaign itself.""" agent, err = require_agent(context) @@ -605,7 +605,7 @@ async def recall_sibling_sessions( ) async def summarize_campaign_history( campaign_id: str, - context: dict = None, + context: dict | None = None, ) -> str: """Compact campaign-progress summary for resolution-mode reasoning.""" agent, err = require_agent(context) @@ -665,7 +665,7 @@ async def summarize_campaign_history( ], ) async def list_imaging_candidates( - context: dict = None, + context: dict | None = None, ) -> str: """Full deterministic listing of unblocked imaging plan items.""" agent, err = require_agent(context) diff --git a/gently/app/tools/session_tools.py b/gently/app/tools/session_tools.py index c1284442..bfe79988 100644 --- a/gently/app/tools/session_tools.py +++ b/gently/app/tools/session_tools.py @@ -20,7 +20,7 @@ category=ToolCategory.ANALYSIS, ) async def assess_image_quality( - embryo_id: str = None, suggest_parameters: bool = True, context: dict = None + embryo_id: str | None = None, suggest_parameters: bool = True, context: dict | None = None ) -> str: """Assess image quality and suggest improvements""" agent, err = require_agent(context) @@ -143,7 +143,7 @@ async def assess_image_quality( ), category=ToolCategory.DATA, ) -def get_session_stats(context: dict = None) -> str: +def get_session_stats(context: dict | None = None) -> str: """Get session statistics from interaction logger""" agent, err = require_agent(context) if err: @@ -172,7 +172,9 @@ def get_session_stats(context: dict = None) -> str: description="Compare developmental progress across multiple embryos in the current experiment", category=ToolCategory.ANALYSIS, ) -def compare_embryo_development(embryo_ids: list[str] = None, context: dict = None) -> str: +def compare_embryo_development( + embryo_ids: list[str] | None = None, context: dict | None = None +) -> str: """Compare embryo development""" agent, err = require_agent(context) if err: @@ -234,7 +236,7 @@ def compare_embryo_development(embryo_ids: list[str] = None, context: dict = Non ), category=ToolCategory.DATA, ) -def analyze_corrections(limit: int = 50, context: dict = None) -> str: +def analyze_corrections(limit: int = 50, context: dict | None = None) -> str: """Analyze correction patterns""" agent, err = require_agent(context) if err: @@ -294,7 +296,7 @@ def analyze_corrections(limit: int = 50, context: dict = None) -> str: description="Export interaction logs for external analysis", category=ToolCategory.DATA, ) -def export_interaction_log(format: str = "summary", context: dict = None) -> str: +def export_interaction_log(format: str = "summary", context: dict | None = None) -> str: """Export interaction log""" agent, err = require_agent(context) if err: @@ -359,7 +361,7 @@ def export_interaction_log(format: str = "summary", context: dict = None) -> str ], ) def import_embryos_from_session( - session_id: str, clear_existing: bool = False, context: dict = None + session_id: str, clear_existing: bool = False, context: dict | None = None ) -> str: """ Import embryos from another session. @@ -415,7 +417,7 @@ def import_embryos_from_session( ToolExample("List recent sessions", {"limit": 5}), ], ) -def list_sessions(limit: int = 20, context: dict = None) -> str: +def list_sessions(limit: int = 20, context: dict | None = None) -> str: """ List available sessions. diff --git a/gently/app/tools/stage_tools.py b/gently/app/tools/stage_tools.py index 88a43871..39f433f3 100644 --- a/gently/app/tools/stage_tools.py +++ b/gently/app/tools/stage_tools.py @@ -92,7 +92,7 @@ async def get_stage_position(context: dict) -> str: ToolExample("Move stage to coordinates 1200, -600", {"x": 1200, "y": -600}), ], ) -async def move_stage(x: float, y: float, context: dict = None) -> str: +async def move_stage(x: float, y: float, context: dict | None = None) -> str: """Move stage to arbitrary XY coordinates""" client = context.get("client") diff --git a/gently/app/tools/timelapse_tools.py b/gently/app/tools/timelapse_tools.py index de7ece90..c794b9ac 100644 --- a/gently/app/tools/timelapse_tools.py +++ b/gently/app/tools/timelapse_tools.py @@ -22,8 +22,8 @@ async def generate_bluesky_plan( goal: str, embryo_ids: list[str], plan_type: str = "adaptive_timelapse", - parameters: dict = None, - context: dict = None, + parameters: dict | None = None, + context: dict | None = None, ) -> str: """Generate Bluesky plan""" agent = context.get("agent") @@ -58,12 +58,12 @@ async def generate_bluesky_plan( requires_microscope=True, ) async def start_adaptive_timelapse( - embryo_ids: list[str] = None, + embryo_ids: list[str] | None = None, stop_condition: str = "manual", interval_seconds: float = 120.0, - condition_value: int = None, + condition_value: int | None = None, monitoring_mode: str | None = None, - context: dict = None, + context: dict | None = None, ) -> str: """Start adaptive timelapse in background""" agent, err = require_agent(context) @@ -121,7 +121,7 @@ async def start_adaptive_timelapse( description="Get current status of the running timelapse including per-embryo progress", category=ToolCategory.EXPERIMENT, ) -def get_timelapse_status(context: dict = None) -> str: +def get_timelapse_status(context: dict | None = None) -> str: """Get timelapse status""" agent, err = require_agent(context) if err: @@ -176,9 +176,9 @@ def get_timelapse_status(context: dict = None) -> str: ) async def modify_timelapse_embryo( embryo_id: str, - stop_condition: str = None, - condition_value: int = None, - context: dict = None, + stop_condition: str | None = None, + condition_value: int | None = None, + context: dict | None = None, ) -> str: """Modify embryo parameters during timelapse (stop condition only - interval is global)""" agent, err = require_agent(context) @@ -211,9 +211,9 @@ async def modify_timelapse_embryo( ) async def add_embryo_to_timelapse( embryo_id: str, - stop_condition: str = None, - condition_value: int = None, - context: dict = None, + stop_condition: str | None = None, + condition_value: int | None = None, + context: dict | None = None, ) -> str: """Add an embryo to a running timelapse (uses global interval)""" agent, err = require_agent(context) @@ -242,7 +242,7 @@ async def add_embryo_to_timelapse( requires_microscope=True, ) async def stop_timelapse_embryo( - embryo_id: str, reason: str = "user_request", context: dict = None + embryo_id: str, reason: str = "user_request", context: dict | None = None ) -> str: """Stop imaging a specific embryo""" agent, err = require_agent(context) @@ -266,7 +266,7 @@ async def stop_timelapse_embryo( category=ToolCategory.EXPERIMENT, requires_microscope=True, ) -async def stop_timelapse(reason: str = "user_request", context: dict = None) -> str: +async def stop_timelapse(reason: str = "user_request", context: dict | None = None) -> str: """Stop entire timelapse""" agent, err = require_agent(context) if err: @@ -289,7 +289,7 @@ async def stop_timelapse(reason: str = "user_request", context: dict = None) -> category=ToolCategory.EXPERIMENT, requires_microscope=True, ) -async def pause_timelapse(context: dict = None) -> str: +async def pause_timelapse(context: dict | None = None) -> str: """Pause timelapse""" agent, err = require_agent(context) if err: @@ -312,7 +312,7 @@ async def pause_timelapse(context: dict = None) -> str: category=ToolCategory.EXPERIMENT, requires_microscope=True, ) -async def resume_timelapse(context: dict = None) -> str: +async def resume_timelapse(context: dict | None = None) -> str: """Resume timelapse""" agent, err = require_agent(context) if err: @@ -337,7 +337,7 @@ async def resume_timelapse(context: dict = None) -> str: ), category=ToolCategory.EXPERIMENT, ) -def add_stop_condition(embryo_id: str, condition: str, context: dict = None) -> str: +def add_stop_condition(embryo_id: str, condition: str, context: dict | None = None) -> str: """ Add an additional stop condition to an embryo in a running timelapse. @@ -408,8 +408,8 @@ def add_stop_condition(embryo_id: str, condition: str, context: dict = None) -> def add_interval_speedup_rule( trigger_stage: str, new_interval_seconds: float = 30.0, - embryo_ids: list[str] = None, - context: dict = None, + embryo_ids: list[str] | None = None, + context: dict | None = None, ) -> str: """Add interval speedup rule based on developmental stage""" agent, err = require_agent(context) @@ -444,7 +444,9 @@ def add_interval_speedup_rule( ), category=ToolCategory.EXPERIMENT, ) -def enable_pre_hatching_speedup(fast_interval_seconds: float = 30.0, context: dict = None) -> str: +def enable_pre_hatching_speedup( + fast_interval_seconds: float = 30.0, context: dict | None = None +) -> str: """Enable pre-hatching speedup based on developmental stage""" agent, err = require_agent(context) if err: @@ -476,7 +478,7 @@ def enable_pre_hatching_speedup(fast_interval_seconds: float = 30.0, context: di description="Use Claude Vision to classify the current developmental stage of an embryo", category=ToolCategory.ANALYSIS, ) -async def classify_embryo_stage(embryo_id: str, context: dict = None) -> str: +async def classify_embryo_stage(embryo_id: str, context: dict | None = None) -> str: """Classify embryo stage""" agent, err = require_agent(context) if err: @@ -538,7 +540,7 @@ async def classify_embryo_stage(embryo_id: str, context: dict = None) -> str: description="Get the developmental stage progression history for an embryo", category=ToolCategory.ANALYSIS, ) -def get_stage_history(embryo_id: str, context: dict = None) -> str: +def get_stage_history(embryo_id: str, context: dict | None = None) -> str: """Get stage history""" agent, err = require_agent(context) if err: @@ -637,7 +639,9 @@ def _perceiver_hatching_estimate(session) -> float | None: ), category=ToolCategory.ANALYSIS, ) -def predict_hatching(embryo_id: str = None, all_embryos: bool = False, context: dict = None) -> str: +def predict_hatching( + embryo_id: str | None = None, all_embryos: bool = False, context: dict | None = None +) -> str: """Predict hatching time with confidence intervals""" agent, err = require_agent(context) if err: @@ -754,7 +758,9 @@ def _perc_line(eid: str): ToolExample("Turn off autonomy", {"mode": "off"}), ], ) -def set_autonomy(mode: str = None, enabled: bool = None, context: dict = None) -> str: +def set_autonomy( + mode: str | None = None, enabled: bool | None = None, context: dict | None = None +) -> str: """Set the wake-router mode (off/ask/auto). `enabled` kept for back-compat.""" agent, err = require_agent(context) if err: @@ -806,7 +812,7 @@ def set_autonomy(mode: str = None, enabled: bool = None, context: dict = None) - ToolExample("Slow everything down to 10 minutes", {"new_interval_seconds": 600}), ], ) -def modify_timelapse_interval(new_interval_seconds: float, context: dict = None) -> str: +def modify_timelapse_interval(new_interval_seconds: float, context: dict | None = None) -> str: """Globally re-anchor the timelapse interval (live).""" agent, err = require_agent(context) if err: @@ -836,9 +842,9 @@ def modify_timelapse_interval(new_interval_seconds: float, context: dict = None) ) def set_embryo_cadence( embryo_id: str, - new_interval_seconds: float = None, - new_phase: str = None, - context: dict = None, + new_interval_seconds: float | None = None, + new_phase: str | None = None, + context: dict | None = None, ) -> str: """Per-embryo cadence change routed through the re-anchoring path.""" agent, err = require_agent(context) @@ -897,9 +903,9 @@ def set_embryo_cadence( ], ) def set_photodose_budget( - base_dose_budget_ms: float = None, + base_dose_budget_ms: float | None = None, resume_paused: bool = True, - context: dict = None, + context: dict | None = None, ) -> str: """Set/clear the photodose budget; optionally resume budget-paused embryos.""" agent, err = require_agent(context) @@ -951,7 +957,7 @@ def set_photodose_budget( category=ToolCategory.ANALYSIS, examples=[ToolExample("How much light has each embryo gotten?", {})], ) -def get_photodose_status(context: dict = None) -> str: +def get_photodose_status(context: dict | None = None) -> str: """Read-only photodose / budget status across embryos.""" agent, err = require_agent(context) if err: @@ -1017,7 +1023,7 @@ def get_photodose_status(context: dict = None) -> str: ) def enable_monitoring_mode( mode_name: str, - context: dict = None, + context: dict | None = None, ) -> str: """Install a named reactive monitoring mode on the orchestrator.""" agent, err = require_agent(context) @@ -1050,7 +1056,7 @@ def enable_monitoring_mode( def add_test_onset_speedup( fast_interval: float = 60.0, embryo_ids: list[str] | None = None, - context: dict = None, + context: dict | None = None, ) -> str: """Install the canonical signal-onset cadence speedup rule.""" agent, err = require_agent(context) @@ -1094,7 +1100,7 @@ def add_test_saturation_rampdown( ceiling_pct: float = 6.0, confirm_timepoints: int = 0, embryo_ids: list[str] | None = None, - context: dict = None, + context: dict | None = None, ) -> str: """Install the canonical 488 saturation rampdown power rule.""" agent, err = require_agent(context) @@ -1146,7 +1152,7 @@ def queue_burst( mode: str = "1hz", num_slices: int = 1, force: bool = False, - context: dict = None, + context: dict | None = None, ) -> str: """Queue an exclusive burst acquisition for one embryo.""" agent, err = require_agent(context) diff --git a/gently/app/tools/volume_tools.py b/gently/app/tools/volume_tools.py index b4893e10..02f1afd0 100644 --- a/gently/app/tools/volume_tools.py +++ b/gently/app/tools/volume_tools.py @@ -30,10 +30,10 @@ ) async def view_image( title: str = "Bottom Camera Image", - exposure_ms: float = None, + exposure_ms: float | None = None, show: bool = True, show_embryos: bool = True, - context: dict = None, + context: dict | None = None, ) -> str: """Capture and display bottom camera image with embryo annotations""" client = context.get("client") @@ -146,10 +146,10 @@ async def view_image( ], ) async def view_volume( - embryo_id: str = None, - timepoint: int = None, - file_path: str = None, - context: dict = None, + embryo_id: str | None = None, + timepoint: int | None = None, + file_path: str | None = None, + context: dict | None = None, ) -> str: """Open a volume in the browser-based viewer (no blocking desktop window).""" from pathlib import Path @@ -239,7 +239,7 @@ async def view_volume( ToolExample("List all volumes", {}), ], ) -async def list_volumes(embryo_id: str = None, context: dict = None) -> str: +async def list_volumes(embryo_id: str | None = None, context: dict | None = None) -> str: """List available volumes""" agent, err = require_agent(context) if err: diff --git a/gently/app/video_maker.py b/gently/app/video_maker.py index fff408fb..f081ae23 100644 --- a/gently/app/video_maker.py +++ b/gently/app/video_maker.py @@ -140,7 +140,7 @@ def create_timelapse_video( output_path: Path, fps: int = 10, add_timestamps: bool = True, - embryo_id: str = None, + embryo_id: str | None = None, progress_callback=None, ) -> dict: """ diff --git a/gently/core/coordinates.py b/gently/core/coordinates.py index 0033756a..9948ad97 100644 --- a/gently/core/coordinates.py +++ b/gently/core/coordinates.py @@ -54,7 +54,7 @@ def pixel_to_stage_position( image_center_y: float, stage_x: float, stage_y: float, - um_per_pixel: float = None, + um_per_pixel: float | None = None, ) -> tuple[float, float]: """ Convert pixel coordinates to stage position (for embryo POSITION calculation). @@ -114,7 +114,7 @@ def stage_to_pixel_position( current_stage_y: float, image_center_x: float, image_center_y: float, - um_per_pixel: float = None, + um_per_pixel: float | None = None, ) -> tuple[float, float]: """ Convert stage position to pixel coordinates (for DISPLAY/visualization). @@ -154,7 +154,7 @@ def stage_to_pixel_position( def pixel_displacement_to_stage_movement( - pixel_displacement_x: float, pixel_displacement_y: float, um_per_pixel: float = None + pixel_displacement_x: float, pixel_displacement_y: float, um_per_pixel: float | None = None ) -> tuple[float, float]: """ Convert pixel displacement to stage MOVEMENT (for centering an embryo). diff --git a/gently/core/file_store.py b/gently/core/file_store.py index b9658c0c..8cd23095 100644 --- a/gently/core/file_store.py +++ b/gently/core/file_store.py @@ -328,9 +328,9 @@ def _generate_projection( def create_session( self, session_id: str, - name: str = None, - description: str = None, - metadata: dict = None, + name: str | None = None, + description: str | None = None, + metadata: dict | None = None, ) -> str: """Create a new session. Returns session_id.""" # If session already exists, return silently (matches INSERT OR IGNORE) @@ -477,14 +477,14 @@ def register_embryo( self, session_id: str, embryo_id: str, - embryo_uid: str = None, - nickname: str = None, - position_x: float = None, - position_y: float = None, - position_coarse: dict = None, - position_fine: dict = None, - calibration: dict = None, - role: str = None, + embryo_uid: str | None = None, + nickname: str | None = None, + position_x: float | None = None, + position_y: float | None = None, + position_coarse: dict | None = None, + position_fine: dict | None = None, + calibration: dict | None = None, + role: str | None = None, ) -> None: """Register or update an embryo in a session. @@ -605,7 +605,7 @@ def put_volume( embryo_id: str, timepoint: int, volume: np.ndarray, - metadata: dict = None, + metadata: dict | None = None, ) -> Path: """ Write a volume to disk, generate a JPEG projection, write sidecar metadata. @@ -655,7 +655,7 @@ def register_volume( embryo_id: str, timepoint: int, incoming_path: Path, - metadata: dict = None, + metadata: dict | None = None, volume_data: np.ndarray = None, ) -> Path: """ @@ -735,7 +735,7 @@ def get_volume_path(self, session_id: str, embryo_id: str, timepoint: int) -> Pa return vol_path return None - def list_volumes(self, session_id: str, embryo_id: str = None) -> list[VolumeInfo]: + def list_volumes(self, session_id: str, embryo_id: str | None = None) -> list[VolumeInfo]: """List volume metadata by scanning sidecar YAML files on disk.""" sd = self._session_dir(session_id) if sd is None: @@ -778,7 +778,7 @@ def list_volumes(self, session_id: str, embryo_id: str = None) -> list[VolumeInf result.sort(key=lambda v: (v["embryo_id"], v["timepoint"])) return result - def get_acquisition_params(self, session_id: str, embryo_id: str = None) -> dict | None: + def get_acquisition_params(self, session_id: str, embryo_id: str | None = None) -> dict | None: """ Get acquisition parameters from the most recent volume sidecar. @@ -895,7 +895,7 @@ def register_snapshot( session_id: str, source: str, incoming_path: Path, - metadata: dict = None, + metadata: dict | None = None, ) -> Path: """Move a transient TIFF from incoming/ to ``snapshots/``.""" incoming_path = Path(incoming_path) @@ -942,7 +942,7 @@ def register_snapshot( logger.debug("register_snapshot: %s -> %s", incoming_path.name, canonical) return canonical - def list_snapshots(self, session_id: str, source: str = None) -> list[dict[str, Any]]: + def list_snapshots(self, session_id: str, source: str | None = None) -> list[dict[str, Any]]: """List snapshot records for a session, optionally filtered by source.""" sd = self._session_dir(session_id) if sd is None: @@ -1018,10 +1018,10 @@ def create_perception_run( session_id: str, name: str, method: str, - model_name: str = None, + model_name: str | None = None, trace_type: str = "perception", source: str = "live", - config: dict = None, + config: dict | None = None, ) -> int: """Create a perception run. Returns run_id (auto-increment).""" runs = self._load_perception_runs(session_id) @@ -1047,7 +1047,7 @@ def create_perception_run( return run_id def complete_perception_run( - self, run_id: int, status: str = "completed", error_message: str = None + self, run_id: int, status: str = "completed", error_message: str | None = None ) -> None: """Mark a perception run as completed or failed. @@ -1071,14 +1071,14 @@ def store_prediction( embryo_id: str, timepoint: int, predicted_stage: str, - confidence: float = None, - reasoning: str = None, + confidence: float | None = None, + reasoning: str | None = None, is_transitional: bool = False, - execution_time_ms: float = None, - trace_data: dict = None, - observed_features: dict = None, - ground_truth_stage: str = None, - is_correct: int = None, + execution_time_ms: float | None = None, + trace_data: dict | None = None, + observed_features: dict | None = None, + ground_truth_stage: str | None = None, + is_correct: int | None = None, ) -> int: """ Append a prediction to predictions.jsonl and optionally write trace JSON. @@ -1133,8 +1133,8 @@ def store_prediction( def get_predictions( self, session_id: str, - embryo_id: str = None, - run_id: int = None, + embryo_id: str | None = None, + run_id: int | None = None, ) -> list[PredictionInfo]: """Query predictions with optional filters.""" sd = self._session_dir(session_id) @@ -1174,9 +1174,9 @@ def set_ground_truth( embryo_id: str, stage: str, start_timepoint: int, - end_timepoint: int = None, - annotator: str = None, - notes: str = None, + end_timepoint: int | None = None, + annotator: str | None = None, + notes: str | None = None, ) -> None: """Insert or update a ground-truth annotation.""" ed = self._embryo_dir(session_id, embryo_id) diff --git a/gently/core/store.py b/gently/core/store.py index e070d734..d33764da 100644 --- a/gently/core/store.py +++ b/gently/core/store.py @@ -279,9 +279,9 @@ def _abs_path(self, rel_path: str) -> Path: def create_session( self, session_id: str, - name: str = None, - description: str = None, - metadata: dict = None, + name: str | None = None, + description: str | None = None, + metadata: dict | None = None, ) -> str: """Create a new session. Returns session_id.""" now = self._now() @@ -355,11 +355,11 @@ def register_embryo( self, session_id: str, embryo_id: str, - embryo_uid: str = None, - nickname: str = None, - position_x: float = None, - position_y: float = None, - calibration: dict = None, + embryo_uid: str | None = None, + nickname: str | None = None, + position_x: float | None = None, + position_y: float | None = None, + calibration: dict | None = None, ): """Register or update an embryo in a session.""" now = self._now() @@ -433,7 +433,7 @@ def put_volume( embryo_id: str, timepoint: int, volume: np.ndarray, - metadata: dict = None, + metadata: dict | None = None, ) -> Path: """ Write a volume to disk, generate a JPEG projection, insert DB rows. @@ -491,7 +491,7 @@ def register_volume( embryo_id: str, timepoint: int, incoming_path: Path, - metadata: dict = None, + metadata: dict | None = None, volume_data: np.ndarray = None, ) -> Path: """ @@ -575,7 +575,7 @@ def register_snapshot( session_id: str, source: str, incoming_path: Path, - metadata: dict = None, + metadata: dict | None = None, ) -> Path: """Move a transient TIFF from *incoming/* to ``snapshots/{session}/``. @@ -633,7 +633,7 @@ def register_snapshot( logger.debug("register_snapshot: %s -> %s", incoming_path.name, canonical) return canonical - def list_snapshots(self, session_id: str, source: str = None) -> list[dict[str, Any]]: + def list_snapshots(self, session_id: str, source: str | None = None) -> list[dict[str, Any]]: """List snapshot records for a session, optionally filtered by source.""" if source: rows = self._conn.execute( @@ -706,7 +706,7 @@ def get_volume_path(self, session_id: str, embryo_id: str, timepoint: int) -> Pa return None return self._abs_path(row["file_path"]) - def list_volumes(self, session_id: str, embryo_id: str = None) -> list[VolumeInfo]: + def list_volumes(self, session_id: str, embryo_id: str | None = None) -> list[VolumeInfo]: """List volume metadata rows for a session (optionally filtered).""" if embryo_id: rows = self._conn.execute( @@ -728,7 +728,7 @@ def list_volumes(self, session_id: str, embryo_id: str = None) -> list[VolumeInf result.append(d) return result - def get_acquisition_params(self, session_id: str, embryo_id: str = None) -> dict | None: + def get_acquisition_params(self, session_id: str, embryo_id: str | None = None) -> dict | None: """ Get the acquisition parameters used in a session. @@ -887,10 +887,10 @@ def create_perception_run( session_id: str, name: str, method: str, - model_name: str = None, + model_name: str | None = None, trace_type: str = "perception", source: str = "live", - config: dict = None, + config: dict | None = None, ) -> int: """Create a perception run. Returns run_id.""" now = self._now() @@ -920,14 +920,14 @@ def store_prediction( embryo_id: str, timepoint: int, predicted_stage: str, - confidence: float = None, - reasoning: str = None, + confidence: float | None = None, + reasoning: str | None = None, is_transitional: bool = False, - execution_time_ms: float = None, - trace_data: dict = None, - observed_features: dict = None, - ground_truth_stage: str = None, - is_correct: int = None, + execution_time_ms: float | None = None, + trace_data: dict | None = None, + observed_features: dict | None = None, + ground_truth_stage: str | None = None, + is_correct: int | None = None, ) -> int: """ Insert a prediction row. Optionally writes trace JSON file. @@ -981,7 +981,7 @@ def store_prediction( return cursor.lastrowid def complete_perception_run( - self, run_id: int, status: str = "completed", error_message: str = None + self, run_id: int, status: str = "completed", error_message: str | None = None ): """Mark a perception run as completed or failed.""" now = self._now() @@ -995,8 +995,8 @@ def complete_perception_run( def get_predictions( self, session_id: str, - embryo_id: str = None, - run_id: int = None, + embryo_id: str | None = None, + run_id: int | None = None, ) -> list[PredictionInfo]: """Query predictions with optional filters.""" clauses = ["session_id = ?"] @@ -1033,9 +1033,9 @@ def set_ground_truth( embryo_id: str, stage: str, start_timepoint: int, - end_timepoint: int = None, - annotator: str = None, - notes: str = None, + end_timepoint: int | None = None, + annotator: str | None = None, + notes: str | None = None, ): """Insert or update a ground-truth annotation.""" with self._tx(): diff --git a/gently/dataset/explorer_server.py b/gently/dataset/explorer_server.py index 510ddd42..3c9e816d 100644 --- a/gently/dataset/explorer_server.py +++ b/gently/dataset/explorer_server.py @@ -505,7 +505,9 @@ def _generate_color(self, client_id: str) -> str: hash_val = sum(ord(c) for c in client_id) return self.AVATAR_COLORS[hash_val % len(self.AVATAR_COLORS)] - async def connect(self, websocket: WebSocket, client_id: str = None, name: str = None): + async def connect( + self, websocket: WebSocket, client_id: str | None = None, name: str | None = None + ): await websocket.accept() # Generate defaults if not provided diff --git a/gently/hardware/dispim/client.py b/gently/hardware/dispim/client.py index f44ff8db..9401fd37 100644 --- a/gently/hardware/dispim/client.py +++ b/gently/hardware/dispim/client.py @@ -256,7 +256,7 @@ async def _api_get(self, path: str) -> dict: async with self._session.get(f"{self.http_url}{path}") as resp: return await resp.json() - async def _api_post(self, path: str, json: dict = None) -> dict: + async def _api_post(self, path: str, json: dict | None = None) -> dict: """POST request using the shared session.""" self._ensure_connected() async with self._session.post(f"{self.http_url}{path}", json=json) as resp: @@ -265,7 +265,7 @@ async def _api_post(self, path: str, json: dict = None) -> dict: async def _submit_plan_and_wait( self, plan_name: str, - kwargs: dict = None, + kwargs: dict | None = None, timeout: float = 120.0, ) -> dict: """Submit a Bluesky plan to the server and wait for completion. @@ -541,11 +541,11 @@ async def acquire_volume( galvo_center: float = 0.0, piezo_amplitude: float = 25.0, piezo_center: float = 50.0, - laser_config: str = None, - laser_power_488_pct: float = None, - laser_power_561_pct: float = None, - laser_power_405_pct: float = None, - laser_power_637_pct: float = None, + laser_config: str | None = None, + laser_power_488_pct: float | None = None, + laser_power_561_pct: float | None = None, + laser_power_405_pct: float | None = None, + laser_power_637_pct: float | None = None, **kwargs, ) -> dict: """ @@ -629,12 +629,12 @@ async def acquire_burst( galvo_center: float = 0.0, piezo_amplitude: float = 25.0, piezo_center: float = 50.0, - laser_config: str = None, - laser_power_488_pct: float = None, - laser_power_561_pct: float = None, - laser_power_405_pct: float = None, - laser_power_637_pct: float = None, - timeout: float = None, + laser_config: str | None = None, + laser_power_488_pct: float | None = None, + laser_power_561_pct: float | None = None, + laser_power_405_pct: float | None = None, + laser_power_637_pct: float | None = None, + timeout: float | None = None, ) -> dict: """ Acquire ``frames`` volumes back-to-back as a single device-layer plan. @@ -956,7 +956,9 @@ async def get_bottom_camera_exposure(self) -> dict: """Get current bottom camera exposure time.""" return await self._api_get("/api/camera/exposure") - async def capture_bottom_image(self, use_led: bool = False, exposure_ms: float = None) -> dict: + async def capture_bottom_image( + self, use_led: bool = False, exposure_ms: float | None = None + ) -> dict: """ Capture image from bottom camera. @@ -1000,7 +1002,7 @@ async def detect_embryos( objective_mag: float = DEFAULT_OBJECTIVE_MAG, use_claude_review: bool = True, min_confidence: float = 0.7, - exposure_ms: float = None, + exposure_ms: float | None = None, brightness_percentile: float = 99.0, min_area: int = 5000, max_area: int = 150000, @@ -1088,7 +1090,7 @@ async def detect_embryos( } async def _get_detection_image( - self, detection_result: dict, exposure_ms: float = None + self, detection_result: dict, exposure_ms: float | None = None ) -> np.ndarray | None: """Load or capture an image for the detection editor.""" image_path = detection_result.get("image_path") @@ -1113,10 +1115,10 @@ async def view_image( self, image: np.ndarray = None, title: str = "Image View", - exposure_ms: float = None, - save_path: str = None, + exposure_ms: float | None = None, + save_path: str | None = None, show: bool = True, - embryo_annotations: list = None, + embryo_annotations: list | None = None, ) -> dict: """Save a bottom-camera image to disk (replaces the napari display). @@ -1224,7 +1226,7 @@ async def view_embryos( async def capture_for_marking( self, - exposure_ms: float = None, + exposure_ms: float | None = None, ) -> dict: """ Capture a bottom-camera image for manual marking in the map view. diff --git a/gently/hardware/dispim/device_layer.py b/gently/hardware/dispim/device_layer.py index bc8daead..6d606621 100644 --- a/gently/hardware/dispim/device_layer.py +++ b/gently/hardware/dispim/device_layer.py @@ -1260,7 +1260,7 @@ async def _plan_executor(self): if len(self._plan_execution_log) > 1000: self._plan_execution_log = self._plan_execution_log[-1000:] - async def submit_plan(self, plan_name: str, kwargs: dict = None) -> dict: + async def submit_plan(self, plan_name: str, kwargs: dict | None = None) -> dict: """Submit a plan and wait for completion""" kwargs = kwargs or {} @@ -2945,7 +2945,7 @@ async def health_check(self) -> dict: base["sam_loaded"] = self._sam_detector is not None return base - async def run(self, host: str = None, port: int = None): + async def run(self, host: str | None = None, port: int | None = None): """Start the server and run until interrupted.""" if host is not None: self.host = host diff --git a/gently/hardware/dispim/devices/acquisition.py b/gently/hardware/dispim/devices/acquisition.py index ad8080ce..9b2658ef 100644 --- a/gently/hardware/dispim/devices/acquisition.py +++ b/gently/hardware/dispim/devices/acquisition.py @@ -97,11 +97,11 @@ def configure( piezo_amplitude: float, piezo_center: float, laser_config: str = "488 and 561", - laser_power_488_pct: float = None, - laser_power_561_pct: float = None, - laser_power_405_pct: float = None, - laser_power_637_pct: float = None, - timing_params: dict = None, + laser_power_488_pct: float | None = None, + laser_power_561_pct: float | None = None, + laser_power_405_pct: float | None = None, + laser_power_637_pct: float | None = None, + timing_params: dict | None = None, ): """ Configure all devices for hardware-triggered volume acquisition. diff --git a/gently/hardware/dispim/devices/optical.py b/gently/hardware/dispim/devices/optical.py index 4108a700..a6544cc0 100644 --- a/gently/hardware/dispim/devices/optical.py +++ b/gently/hardware/dispim/devices/optical.py @@ -20,7 +20,7 @@ class DiSPIMLED: Device-agnostic: any plan that sets device state will work """ - def __init__(self, core: pymmcore.CMMCore, name: str = "LED", group_name: str = None): + def __init__(self, core: pymmcore.CMMCore, name: str = "LED", group_name: str | None = None): self.core = core self.name = name self.group_name = group_name or name @@ -128,7 +128,7 @@ class DiSPIMLightSource: 637: (0.0, 100.0), } - def __init__(self, core: pymmcore.CMMCore, name: str = "Laser", group_name: str = None): + def __init__(self, core: pymmcore.CMMCore, name: str = "Laser", group_name: str | None = None): self.core = core self.name = name self.group_name = group_name or name diff --git a/gently/hardware/dispim/devices/scanner.py b/gently/hardware/dispim/devices/scanner.py index 79732ef1..1197c5c1 100644 --- a/gently/hardware/dispim/devices/scanner.py +++ b/gently/hardware/dispim/devices/scanner.py @@ -367,7 +367,7 @@ def configure_for_volume_acquisition( galvo_amplitude: float, galvo_center: float, num_slices: int, - timing_params: dict = None, + timing_params: dict | None = None, ): """ Configure scanner for hardware-triggered volume acquisition. diff --git a/gently/hardware/dispim/plans/acquisition.py b/gently/hardware/dispim/plans/acquisition.py index 84e49d02..02aabba8 100644 --- a/gently/hardware/dispim/plans/acquisition.py +++ b/gently/hardware/dispim/plans/acquisition.py @@ -789,10 +789,10 @@ def acquire_single_volume_plan( piezo_amplitude: float = 25.0, piezo_center: float = 50.0, laser_config: str = "488 and 561", - laser_power_488_pct: float = None, - laser_power_561_pct: float = None, - laser_power_405_pct: float = None, - laser_power_637_pct: float = None, + laser_power_488_pct: float | None = None, + laser_power_561_pct: float | None = None, + laser_power_405_pct: float | None = None, + laser_power_637_pct: float | None = None, timing_params: dict | None = None, metadata: dict | None = None, ): @@ -924,10 +924,10 @@ def burst_plan( piezo_amplitude: float = 25.0, piezo_center: float = 50.0, laser_config: str = "488 only", - laser_power_488_pct: float = None, - laser_power_561_pct: float = None, - laser_power_405_pct: float = None, - laser_power_637_pct: float = None, + laser_power_488_pct: float | None = None, + laser_power_561_pct: float | None = None, + laser_power_405_pct: float | None = None, + laser_power_637_pct: float | None = None, timing_params: dict | None = None, metadata: dict | None = None, ): diff --git a/gently/harness/memory/interface.py b/gently/harness/memory/interface.py index 7eba1774..2e27db8d 100644 --- a/gently/harness/memory/interface.py +++ b/gently/harness/memory/interface.py @@ -35,7 +35,7 @@ class AgentMemory: or prompt injection. """ - def __init__(self, context_store, session_id: str = None): + def __init__(self, context_store, session_id: str | None = None): self.store = context_store self.session_id = session_id # Set by startup flow after resolve_plan_context() @@ -243,7 +243,7 @@ def get_awareness_summary(self) -> str: # Briefing layer — auto-briefing at session start # ------------------------------------------------------------------ - def get_session_briefing(self, campaign_id: str = None) -> str: + def get_session_briefing(self, campaign_id: str | None = None) -> str: """Generate a session briefing for new sessions. If campaign_id is provided (or session is linked to a campaign), @@ -455,7 +455,7 @@ def recall_campaigns(self, status: str = "active") -> str: return "\n".join(lines) - def recall_learnings(self, query: str = None, limit: int = 20) -> str: + def recall_learnings(self, query: str | None = None, limit: int = 20) -> str: """Search or list learnings.""" learnings = self.store.get_learnings(limit=max(limit, 50)) @@ -491,7 +491,9 @@ def recall_learnings(self, query: str = None, limit: int = 20) -> str: return "\n".join(lines) - def recall_observations(self, query: str = None, embryo_id: str = None, limit: int = 20) -> str: + def recall_observations( + self, query: str | None = None, embryo_id: str | None = None, limit: int = 20 + ) -> str: """Search or list observations.""" if embryo_id: observations = self.store.get_observations_for_embryo(embryo_id) @@ -531,7 +533,7 @@ def recall_observations(self, query: str = None, embryo_id: str = None, limit: i return "\n".join(lines) - def recall_full_context(self, campaign_id: str = None) -> str: + def recall_full_context(self, campaign_id: str | None = None) -> str: """Full context snapshot — the 'catch me up' method. If campaign_id provided (or session is linked), focuses there. diff --git a/gently/harness/microscope.py b/gently/harness/microscope.py index f76af5c5..6c684469 100644 --- a/gently/harness/microscope.py +++ b/gently/harness/microscope.py @@ -317,7 +317,7 @@ def register_microscope_tools(microscope: Microscope, registry=None) -> int: def _make_handler(pname): """Create an async handler that delegates to microscope.execute().""" - async def handler(context: dict = None, **params): + async def handler(context: dict | None = None, **params): ms = context.get("client") if context else microscope if ms is None: return "Error: microscope not connected" diff --git a/gently/harness/plan_mode/tools/lab_context.py b/gently/harness/plan_mode/tools/lab_context.py index 401db7d6..574a725d 100644 --- a/gently/harness/plan_mode/tools/lab_context.py +++ b/gently/harness/plan_mode/tools/lab_context.py @@ -25,7 +25,7 @@ ) async def query_lab_history( query: str, - context: dict = None, + context: dict | None = None, ) -> str: """Search lab history for relevant context.""" agent = context.get("agent") if context else None @@ -114,7 +114,7 @@ async def query_lab_history( ) async def check_hardware_capability( question: str, - context: dict = None, + context: dict | None = None, ) -> str: """Check hardware capabilities against a question.""" from gently.hardware import get_hardware diff --git a/gently/harness/plan_mode/tools/planning.py b/gently/harness/plan_mode/tools/planning.py index b1a976c4..34785e5f 100644 --- a/gently/harness/plan_mode/tools/planning.py +++ b/gently/harness/plan_mode/tools/planning.py @@ -37,10 +37,10 @@ ) async def create_campaign( description: str, - shorthand: str = None, - target: str = None, - parent_id: str = None, - context: dict = None, + shorthand: str | None = None, + target: str | None = None, + parent_id: str | None = None, + context: dict | None = None, ) -> str: """Create a campaign or sub-campaign (phase).""" agent = context.get("agent") if context else None @@ -112,15 +112,15 @@ async def create_plan_item( campaign_id: str, type: str, title: str, - description: str = None, - spec: dict = None, - inherit_from: str = None, - depends_on: list[str] = None, - phase_number: int = None, + description: str | None = None, + spec: dict | None = None, + inherit_from: str | None = None, + depends_on: list[str] | None = None, + phase_number: int | None = None, phase_order: int = -1, - references: list[dict] = None, - estimated_days: int = None, - context: dict = None, + references: list[dict] | None = None, + estimated_days: int | None = None, + context: dict | None = None, ) -> str: """Create a plan item within a campaign/phase. @@ -198,15 +198,15 @@ async def create_plan_item( ) async def update_plan_item( item_id: str, - status: str = None, - title: str = None, - description: str = None, - outcome: str = None, - spec: dict = None, - references: list[dict] = None, - estimated_days: int = None, - campaign_id: str = None, - context: dict = None, + status: str | None = None, + title: str | None = None, + description: str | None = None, + outcome: str | None = None, + spec: dict | None = None, + references: list[dict] | None = None, + estimated_days: int | None = None, + campaign_id: str | None = None, + context: dict | None = None, ) -> str: """Update a plan item. item_id can be a UUID, task number (e.g. '3'), or phase.task reference (e.g. '1.3'). campaign_id scopes resolution @@ -263,8 +263,8 @@ async def update_plan_item( async def link_plan_items( item_id: str, depends_on_id: str, - campaign_id: str = None, - context: dict = None, + campaign_id: str | None = None, + context: dict | None = None, ) -> str: """Add a dependency between plan items. campaign_id scopes resolution when using shorthand refs (e.g. '1.3') with multiple plans.""" @@ -308,8 +308,8 @@ async def link_plan_items( ) async def get_plan_item_tool( ref: str, - campaign_id: str = None, - context: dict = None, + campaign_id: str | None = None, + context: dict | None = None, ) -> str: """Look up a plan item by natural reference.""" agent = context.get("agent") if context else None @@ -347,7 +347,7 @@ async def get_plan_item_tool( ) async def propose_plan( campaign_id: str, - context: dict = None, + context: dict | None = None, ) -> str: """Render the full plan for review.""" agent = context.get("agent") if context else None @@ -506,7 +506,7 @@ def _format_plan_item(item, store, task_num: str = "") -> str: ) async def get_plan_status( campaign_id: str, - context: dict = None, + context: dict | None = None, ) -> str: """Get plan progress summary.""" agent = context.get("agent") if context else None @@ -576,10 +576,10 @@ async def get_plan_status( async def batch_update_status( campaign_id: str, new_status: str, - outcome: str = None, - phase_number: int = None, - item_type: str = None, - context: dict = None, + outcome: str | None = None, + phase_number: int | None = None, + item_type: str | None = None, + context: dict | None = None, ) -> str: """Batch-update status of plan items.""" agent = context.get("agent") if context else None @@ -659,8 +659,8 @@ async def batch_update_spec( campaign_id: str, field_name: str, field_value: object, - phase_number: int = None, - context: dict = None, + phase_number: int | None = None, + context: dict | None = None, ) -> str: """Batch-update a spec field on imaging items.""" agent = context.get("agent") if context else None @@ -737,10 +737,10 @@ async def batch_update_spec( async def move_plan_item( campaign_id: str, item_ref: str, - to_phase_number: int = None, - to_campaign_id: str = None, - position: int = None, - context: dict = None, + to_phase_number: int | None = None, + to_campaign_id: str | None = None, + position: int | None = None, + context: dict | None = None, ) -> str: """Move a plan item to a different phase.""" agent = context.get("agent") if context else None @@ -799,8 +799,8 @@ async def move_plan_item( ) async def delete_plan_item_tool( item_ref: str, - campaign_id: str = None, - context: dict = None, + campaign_id: str | None = None, + context: dict | None = None, ) -> str: """Delete a plan item.""" agent = context.get("agent") if context else None @@ -856,8 +856,8 @@ async def delete_plan_item_tool( async def reorder_plan_items( campaign_id: str, item_order: list[str], - phase_number: int = None, - context: dict = None, + phase_number: int | None = None, + context: dict | None = None, ) -> str: """Reorder plan items within a phase.""" agent = context.get("agent") if context else None @@ -919,10 +919,10 @@ async def reorder_plan_items( async def update_phase( campaign_id: str, phase_number: int, - description: str = None, - shorthand: str = None, - target: str = None, - context: dict = None, + description: str | None = None, + shorthand: str | None = None, + target: str | None = None, + context: dict | None = None, ) -> str: """Update a phase's metadata.""" agent = context.get("agent") if context else None @@ -963,7 +963,7 @@ async def update_phase( async def delete_phase( campaign_id: str, phase_number: int, - context: dict = None, + context: dict | None = None, ) -> str: """Delete a phase and its contents.""" agent = context.get("agent") if context else None @@ -1017,7 +1017,7 @@ async def delete_phase( async def export_plan( campaign_id: str, include_validation: bool = False, - context: dict = None, + context: dict | None = None, ) -> str: """Export a plan as a shareable markdown document.""" agent = context.get("agent") if context else None @@ -1327,8 +1327,8 @@ async def validate_plan_for_export(campaign_id: str, store) -> str: ) async def snapshot_plan( campaign_id: str, - label: str = None, - context: dict = None, + label: str | None = None, + context: dict | None = None, ) -> str: """Save a snapshot of the current plan.""" agent = context.get("agent") if context else None @@ -1367,7 +1367,7 @@ async def snapshot_plan( ) async def list_plan_versions( campaign_id: str, - context: dict = None, + context: dict | None = None, ) -> str: """List saved plan versions.""" agent = context.get("agent") if context else None @@ -1415,9 +1415,9 @@ async def list_plan_versions( ) async def restore_plan_version( campaign_id: str, - version_id: str = None, - version_number: int = None, - context: dict = None, + version_id: str | None = None, + version_number: int | None = None, + context: dict | None = None, ) -> str: """Restore a plan to a previous snapshot.""" agent = context.get("agent") if context else None diff --git a/gently/harness/plan_mode/tools/research.py b/gently/harness/plan_mode/tools/research.py index 6c3b19b5..dabd92b5 100644 --- a/gently/harness/plan_mode/tools/research.py +++ b/gently/harness/plan_mode/tools/research.py @@ -448,7 +448,7 @@ async def _cgc_search(query: str, field: str = "strain") -> list[dict]: async def search_literature( query: str, max_results: int = 5, - context: dict = None, + context: dict | None = None, ) -> str: """Search PubMed for relevant papers. @@ -590,7 +590,7 @@ async def search_literature( async def search_strains( query: str, organism: str = "celegans", - context: dict = None, + context: dict | None = None, ) -> str: """Search for strains and genes via NCBI Gene + WormBase REST API. @@ -1221,7 +1221,7 @@ async def _doi_to_pmid(doi: str) -> str | None: ) async def read_paper( reference: str, - context: dict = None, + context: dict | None = None, ) -> str: """Read a scientific paper and return its content. diff --git a/gently/harness/plan_mode/tools/templates.py b/gently/harness/plan_mode/tools/templates.py index b75b0e64..03557477 100644 --- a/gently/harness/plan_mode/tools/templates.py +++ b/gently/harness/plan_mode/tools/templates.py @@ -31,8 +31,8 @@ async def save_plan_template( campaign_id: str, name: str, - description: str = None, - context: dict = None, + description: str | None = None, + context: dict | None = None, ) -> str: """Save a campaign as a reusable template.""" agent = context.get("agent") if context else None @@ -65,7 +65,7 @@ async def save_plan_template( category=ToolCategory.UTILITY, ) async def list_templates( - context: dict = None, + context: dict | None = None, ) -> str: """List available plan templates.""" agent = context.get("agent") if context else None @@ -110,8 +110,8 @@ async def list_templates( ) async def apply_template( template_id: str, - overrides: dict = None, - context: dict = None, + overrides: dict | None = None, + context: dict | None = None, ) -> str: """Instantiate a template into a new campaign.""" agent = context.get("agent") if context else None diff --git a/gently/harness/plan_mode/tools/validation.py b/gently/harness/plan_mode/tools/validation.py index 18e06ae6..031918ca 100644 --- a/gently/harness/plan_mode/tools/validation.py +++ b/gently/harness/plan_mode/tools/validation.py @@ -165,7 +165,7 @@ def _normalise_stage(name: str) -> str | None: ) async def validate_plan( campaign_id: str, - context: dict = None, + context: dict | None = None, ) -> str: """Validate a plan and return errors/warnings.""" agent = context.get("agent") if context else None diff --git a/gently/harness/prompts/manager.py b/gently/harness/prompts/manager.py index 9552be76..9405c820 100644 --- a/gently/harness/prompts/manager.py +++ b/gently/harness/prompts/manager.py @@ -47,7 +47,7 @@ def __init__(self, claude_client, model): self.memory = None # AgentMemory instance def update_system_prompt( - self, experiment, client, mode: str, context_summary: str = None, perceiver=None + self, experiment, client, mode: str, context_summary: str | None = None, perceiver=None ) -> str: """ Rebuild system prompt with current experiment state and connection status. diff --git a/gently/harness/prompts/templates.py b/gently/harness/prompts/templates.py index ef2fc3f4..f75fc7a5 100644 --- a/gently/harness/prompts/templates.py +++ b/gently/harness/prompts/templates.py @@ -365,9 +365,9 @@ def build_perception_snapshot(perceiver, embryos) -> str: def build_system_prompt( experiment_state: ExperimentState, - connection_status: dict = None, - context_summary: str = None, - memory_awareness: str = None, + connection_status: dict | None = None, + context_summary: str | None = None, + memory_awareness: str | None = None, microscope=None, perceiver=None, ) -> str: diff --git a/gently/harness/state.py b/gently/harness/state.py index e5b275ab..34b7d8f6 100644 --- a/gently/harness/state.py +++ b/gently/harness/state.py @@ -244,15 +244,15 @@ class EmbryoState: def add_focus_datapoint( self, - z: float = None, + z: float | None = None, secondary_axis: float = 0.0, score: float = 0.0, r_squared: float = 0.0, method: str = "manual", algorithm: str = "fft_bandpass", # Backward-compatible kwargs - galvo: float = None, - piezo: float = None, + galvo: float | None = None, + piezo: float | None = None, ): """ Record a focus measurement for this embryo. @@ -440,7 +440,7 @@ def get_piezo_galvo_fit(self, **kwargs) -> tuple[float, float] | None: def get_focus_drift_rate( self, secondary_position: float = 0.0, - galvo_position: float = None, + galvo_position: float | None = None, min_measurements: int = 3, ) -> float | None: """ @@ -498,7 +498,7 @@ def needs_refocus( self, max_age_minutes: float = 60, secondary_position: float = 0.0, - galvo_position: float = None, + galvo_position: float | None = None, ) -> bool: """ Determine if this embryo needs focus re-measurement. @@ -936,13 +936,13 @@ def notify_embryos_changed(self) -> None: def add_embryo( self, embryo_id: str, - position: dict = None, - calibration: dict = None, + position: dict | None = None, + calibration: dict | None = None, user_label: str | None = None, confidence: float = 0.0, uid: str | None = None, role: str = "test", - position_fine: dict = None, + position_fine: dict | None = None, ): """Register new embryo. diff --git a/gently/harness/tools/registry.py b/gently/harness/tools/registry.py index 30f72cf8..88c37241 100644 --- a/gently/harness/tools/registry.py +++ b/gently/harness/tools/registry.py @@ -400,7 +400,7 @@ def get_claude_schemas(self, has_microscope: bool = False) -> list[dict]: """Get Claude API tool schemas for available tools""" return [tool.to_claude_schema() for tool in self.list_available(has_microscope)] - async def execute(self, tool_name: str, tool_input: dict, context: dict = None) -> str: + async def execute(self, tool_name: str, tool_input: dict, context: dict | None = None) -> str: """ Execute a tool by name diff --git a/gently/log_config.py b/gently/log_config.py index 5d5f75b2..69a673a0 100644 --- a/gently/log_config.py +++ b/gently/log_config.py @@ -20,8 +20,8 @@ def configure_logging( - level: str = None, - log_file: str = None, + level: str | None = None, + log_file: str | None = None, ): """Configure root logger for the Gently system. diff --git a/gently/mesh/verse_map.py b/gently/mesh/verse_map.py index 15265548..7fd36934 100644 --- a/gently/mesh/verse_map.py +++ b/gently/mesh/verse_map.py @@ -152,7 +152,7 @@ def find_microscope_peers(self) -> list[PersistedPeer]: ] return self._sorted_peers(results) - def find_data_peers(self, session_id: str = None) -> list[PersistedPeer]: + def find_data_peers(self, session_id: str | None = None) -> list[PersistedPeer]: """Find peers with data, optionally filtering by session.""" results = [] for p in self._peers.values(): diff --git a/gently/ui/web/connection_manager.py b/gently/ui/web/connection_manager.py index ffaaf11a..11cc3fe4 100644 --- a/gently/ui/web/connection_manager.py +++ b/gently/ui/web/connection_manager.py @@ -54,7 +54,9 @@ def _generate_color(self, client_id: str) -> str: hash_val = sum(ord(c) for c in client_id) return self.AVATAR_COLORS[hash_val % len(self.AVATAR_COLORS)] - async def connect(self, websocket: WebSocket, client_id: str = None, name: str = None): + async def connect( + self, websocket: WebSocket, client_id: str | None = None, name: str | None = None + ): await websocket.accept() # Generate defaults if not provided @@ -168,7 +170,7 @@ async def send_image(self, image_data: ImageData): await self.broadcast({"type": "image", "data": image_data.to_dict()}) async def send_event( - self, event_type: str, data: dict, source: str = None, event_id: str = None + self, event_type: str, data: dict, source: str | None = None, event_id: str | None = None ): """Send event notification to all clients""" await self.broadcast( diff --git a/gently/ui/web/server.py b/gently/ui/web/server.py index 12e47714..c16c548d 100644 --- a/gently/ui/web/server.py +++ b/gently/ui/web/server.py @@ -143,8 +143,8 @@ def __init__( event_bus=None, sessions_dir: str = str(settings.storage.sessions_dir), gently_store=None, - ssl_certfile: str = None, - ssl_keyfile: str = None, + ssl_certfile: str | None = None, + ssl_keyfile: str | None = None, ): super().__init__(name="visualization", service_type="http", host=host, port=port) if not FASTAPI_AVAILABLE: @@ -636,7 +636,7 @@ async def start_marking_session( ) return session_id - async def wait_for_marking(self, session_id: str, timeout: float = None) -> list: + async def wait_for_marking(self, session_id: str, timeout: float | None = None) -> list: """ Wait for a marking session to complete. diff --git a/launch_gently.py b/launch_gently.py index 0663c500..64efdc15 100644 --- a/launch_gently.py +++ b/launch_gently.py @@ -225,7 +225,7 @@ def run_ink_picker(tui_dist: Path, sessions_json: str) -> str | None: async def main( offline: bool = False, - resume_session: str = None, + resume_session: str | None = None, show_sessions: bool = False, pick_session: bool = False, log_level: str = "WARNING", diff --git a/pyproject.toml b/pyproject.toml index 82cc2dcf..bb3d5b40 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -182,7 +182,6 @@ module = [ "gently.app.tools.detection_tools", "gently.app.tools.experiment_tools", "gently.app.tools.focus_tools", - "gently.app.tools.interaction_tools", "gently.app.tools.led_tools", "gently.app.tools.light_source_tools", "gently.app.tools.memory_tools", @@ -194,7 +193,6 @@ module = [ "gently.app.tools.timelapse_tools", "gently.app.tools.volume_tools", "gently.app.video_maker", - "gently.core.coordinates", "gently.core.event_bus", "gently.core.file_store", "gently.core.imaging", @@ -212,8 +210,6 @@ module = [ "gently.hardware.dispim.device_layer", "gently.hardware.dispim.devices.acquisition", "gently.hardware.dispim.devices.camera", - "gently.hardware.dispim.devices.optical", - "gently.hardware.dispim.devices.scanner", "gently.hardware.dispim.devices.test_temperature_controller", "gently.hardware.dispim.plans.acquisition", "gently.hardware.dispim.sam_detection", @@ -226,40 +222,30 @@ module = [ "gently.harness.memory.file_store", "gently.harness.memory.gap_assessment", "gently.harness.memory._intentions", - "gently.harness.memory.interface", "gently.harness.memory._ml_pipelines", "gently.harness.memory.onboarding", "gently.harness.memory._plans", "gently.harness.memory.startup_wizard", "gently.harness.memory._understanding", "gently.harness.microscope", - "gently.harness.plan_mode.tools.lab_context", - "gently.harness.plan_mode.tools.planning", "gently.harness.plan_mode.tools.research", - "gently.harness.plan_mode.tools.templates", - "gently.harness.plan_mode.tools.validation", "gently.harness.prompts.manager", - "gently.harness.prompts.templates", "gently.harness.session.interaction_logger", "gently.harness.session.manager", "gently.harness.session.timeline", - "gently.harness.state", "gently.harness.tools.registry", "gently.log_config", "gently.mesh.capability_provider", "gently.mesh.discovery", "gently.mesh.peer_client", - "gently.mesh.verse_map", "gently.ml.data_loader", "gently.ml.federated", "gently.organisms.celegans.developmental_tracker", - "gently.ui.web.connection_manager", "gently.ui.web.routes.agent_ws", "gently.ui.web.routes.campaigns", "gently.ui.web.routes.data", "gently.ui.web.routes.volumes", "gently.ui.web.routes.websocket", - "gently.ui.web.server", "gently.ui.web.strategy_snapshot", "launch_gently", "scripts.auto_label_examples", diff --git a/scripts/gemini_stage_test.py b/scripts/gemini_stage_test.py index d99d12db..0a9a44b1 100644 --- a/scripts/gemini_stage_test.py +++ b/scripts/gemini_stage_test.py @@ -67,7 +67,7 @@ def ensure_dependencies(): # ============================================================================ -def discover_volumes(session_dir: Path, embryo_id: str = None) -> dict: +def discover_volumes(session_dir: Path, embryo_id: str | None = None) -> dict: """Discover volume files in a session directory.""" if not session_dir.exists(): return {} @@ -258,7 +258,7 @@ def create_embryo_video( embryo_id: str, storage_path: Path, fps: int = 10, - output_path: Path = None, + output_path: Path | None = None, ) -> dict: """ Create timelapse video from embryo images. @@ -496,7 +496,7 @@ def build_stage_classification_prompt(frame_count: int, fps: int, duration_secon def analyze_with_gemini( video_path: str, model: str = "gemini-3-pro-preview", - api_key: str = None, + api_key: str | None = None, frame_count: int = 100, fps: int = 10, duration_seconds: float = 10.0, From 06ced015e4b38c3a4e412ce719704764d0602dbd Mon Sep 17 00:00:00 2001 From: subindevs <36504048+subindevs@users.noreply.github.com> Date: Sun, 14 Jun 2026 23:06:22 +0100 Subject: [PATCH 5/8] Add StoreProtocol for ContextStore memory mixins, fix remaining type errors The four ContextStore mixins (_intentions, _plans, _understanding, _ml_pipelines) call methods/attributes defined on the host class or sibling mixins (_conn, _tx, _now, _gen_id, get_plan_items, create_campaign, etc.), which mypy couldn't see on the mixins themselves. Add gently/harness/memory/_protocols.py declaring a StoreProtocol with these members and have each mixin inherit from it for typing only. Also fixes the remaining errors this didn't cover: two untyped dict literals in _plans.py needed explicit dict[str, Any] annotations, and three _ml_pipelines.py methods returning an Optional lookup right after an insert now assert the row exists. All four mixins are now mypy-clean; removes them from the ignore_errors override list (99 -> 95). --- gently/harness/memory/_intentions.py | 3 +- gently/harness/memory/_ml_pipelines.py | 16 +++++-- gently/harness/memory/_plans.py | 7 +-- gently/harness/memory/_protocols.py | 59 +++++++++++++++++++++++++ gently/harness/memory/_understanding.py | 3 +- pyproject.toml | 4 -- 6 files changed, 79 insertions(+), 13 deletions(-) create mode 100644 gently/harness/memory/_protocols.py diff --git a/gently/harness/memory/_intentions.py b/gently/harness/memory/_intentions.py index de1e4e59..59de4048 100644 --- a/gently/harness/memory/_intentions.py +++ b/gently/harness/memory/_intentions.py @@ -11,6 +11,7 @@ from datetime import datetime from typing import Any +from ._protocols import StoreProtocol from .model import ( Campaign, Intentions, @@ -24,7 +25,7 @@ logger = logging.getLogger(__name__) -class IntentionsMixin: +class IntentionsMixin(StoreProtocol): """Campaign management, projects, session intents, and planned sessions.""" # ------------------------------------------------------------------ diff --git a/gently/harness/memory/_ml_pipelines.py b/gently/harness/memory/_ml_pipelines.py index a14a2553..e15a8eb6 100644 --- a/gently/harness/memory/_ml_pipelines.py +++ b/gently/harness/memory/_ml_pipelines.py @@ -11,6 +11,8 @@ import logging from typing import Any +from ._protocols import StoreProtocol + logger = logging.getLogger(__name__) ML_SCHEMA_SQL = """\ @@ -74,7 +76,7 @@ """ -class MlPipelinesMixin: +class MlPipelinesMixin(StoreProtocol): """ContextStore mixin for ML pipeline management.""" def _ensure_ml_tables(self): @@ -117,7 +119,9 @@ def create_ml_pipeline( now, ), ) - return self.get_ml_pipeline(pipeline_id) + pipeline = self.get_ml_pipeline(pipeline_id) + assert pipeline is not None + return pipeline def get_ml_pipeline(self, pipeline_id: str) -> dict[str, Any] | None: """Get a pipeline by ID.""" @@ -227,7 +231,9 @@ def create_training_run( peer_instance_id, ), ) - return self.get_training_run(run_id) + run = self.get_training_run(run_id) + assert run is not None + return run def get_training_run(self, run_id: str) -> dict[str, Any] | None: """Get a training run by ID.""" @@ -347,7 +353,9 @@ def save_data_assessment( now, ), ) - return self.get_data_assessment(assessment_id) + assessment = self.get_data_assessment(assessment_id) + assert assessment is not None + return assessment def get_data_assessment(self, assessment_id: str) -> dict[str, Any] | None: """Get a data assessment by ID.""" diff --git a/gently/harness/memory/_plans.py b/gently/harness/memory/_plans.py index 5805781a..96e4d18a 100644 --- a/gently/harness/memory/_plans.py +++ b/gently/harness/memory/_plans.py @@ -11,6 +11,7 @@ from datetime import datetime from typing import Any +from ._protocols import StoreProtocol from .model import ( BenchSpec, ImagingSpec, @@ -22,7 +23,7 @@ logger = logging.getLogger(__name__) -class PlansMixin: +class PlansMixin(StoreProtocol): """Plan items, templates, snapshots, and dependency management.""" # ================================================================== @@ -474,7 +475,7 @@ def get_plan_status(self, campaign_id: str) -> dict[str, Any]: campaign_id=campaign_id, include_children=True, ) - result = { + result: dict[str, Any] = { "total": len(items), "completed": 0, "in_progress": 0, @@ -589,7 +590,7 @@ def _serialize_campaign_tree(self, campaign_id: str) -> dict: all_item_ids = [it.id for it in items] serialized_items = [] for item in items: - item_data = { + item_data: dict[str, Any] = { "type": item.type.value, "title": item.title, "description": item.description, diff --git a/gently/harness/memory/_protocols.py b/gently/harness/memory/_protocols.py new file mode 100644 index 00000000..7fddeb77 --- /dev/null +++ b/gently/harness/memory/_protocols.py @@ -0,0 +1,59 @@ +""" +StoreProtocol — typing-only interface for members the memory mixins expect +from their host class (ContextStore) and from each other. + +IntentionsMixin, PlansMixin, UnderstandingMixin, and MlPipelinesMixin are +combined into ContextStore via multiple inheritance. Each mixin calls +methods/attributes defined either on ContextStore itself (_conn, _tx, _now, +_gen_id) or on one of the sibling mixins (e.g. PlansMixin.get_plan_items +called from IntentionsMixin). Declaring this Protocol as a base lets mypy +see those members without introducing a runtime dependency between mixins. +""" + +import sqlite3 +from contextlib import AbstractContextManager +from typing import Protocol + +from .model import Campaign, PlanItem + + +class StoreProtocol(Protocol): + _conn: sqlite3.Connection + + def _tx(self) -> AbstractContextManager[sqlite3.Connection]: ... + + def _now(self) -> str: ... + + def _gen_id(self) -> str: ... + + def get_state(self, key: str) -> str | None: ... + + def get_campaign(self, campaign_id: str) -> Campaign | None: ... + + def get_root_campaigns(self, status: str | None = "active") -> list[Campaign]: ... + + def get_subcampaigns(self, campaign_id: str) -> list[Campaign]: ... + + def create_campaign( + self, + description: str, + shorthand: str | None = None, + summary: str | None = None, + target: str | None = None, + parent_id: str | None = None, + campaign_id: str | None = None, + ) -> str: ... + + def delete_campaign(self, campaign_id: str, cascade: bool = True) -> dict[str, int]: ... + + def update_campaign_progress(self, campaign_id: str, progress: str) -> None: ... + + def get_plan_items( + self, + campaign_id: str | None = None, + status: str | None = None, + type: str | None = None, + include_children: bool = False, + ) -> list[PlanItem]: ... + + def _resolve_campaign_label(self, label: str) -> str | None: ... diff --git a/gently/harness/memory/_understanding.py b/gently/harness/memory/_understanding.py index 3e72d7eb..29cc9c9a 100644 --- a/gently/harness/memory/_understanding.py +++ b/gently/harness/memory/_understanding.py @@ -11,6 +11,7 @@ import sqlite3 from datetime import datetime +from ._protocols import StoreProtocol from .model import ( Attention, Confidence, @@ -31,7 +32,7 @@ logger = logging.getLogger(__name__) -class UnderstandingMixin: +class UnderstandingMixin(StoreProtocol): """Observations, expectations, watchpoints, questions, learnings, embryo understanding, agent state, and batch updates.""" diff --git a/pyproject.toml b/pyproject.toml index bb3d5b40..8f00eacf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -221,12 +221,8 @@ module = [ "gently.harness.memory", "gently.harness.memory.file_store", "gently.harness.memory.gap_assessment", - "gently.harness.memory._intentions", - "gently.harness.memory._ml_pipelines", "gently.harness.memory.onboarding", - "gently.harness.memory._plans", "gently.harness.memory.startup_wizard", - "gently.harness.memory._understanding", "gently.harness.microscope", "gently.harness.plan_mode.tools.research", "gently.harness.prompts.manager", From 4e24a923a7bd17504543515c9fdc1b281ee90db4 Mon Sep 17 00:00:00 2001 From: subindevs <36504048+subindevs@users.noreply.github.com> Date: Sun, 14 Jun 2026 23:31:32 +0100 Subject: [PATCH 6/8] Type the tool-execution context dict, clearing gently.app.tools.* mypy overrides Adds ctx_get(context, key) and retypes the require_* helpers in harness/tools/helpers.py to accept context: dict | None and return non-Optional success values, eliminating the dominant union-attr/arg-type pattern across the tool modules. Applies the same fix to memory_tools' local _get_memory and a handful of independent var-annotated/type issues. Removes all 16 gently.app.tools.* modules from the mypy override list (95 -> 79), cutting the underlying error count from 610 to 369. Co-Authored-By: Claude Sonnet 4.6 --- gently/app/tools/acquisition_tools.py | 14 ++++---- gently/app/tools/calibration_tools.py | 14 ++++---- gently/app/tools/detection_tools.py | 19 +++++------ gently/app/tools/focus_tools.py | 9 +++--- gently/app/tools/led_tools.py | 5 +-- gently/app/tools/light_source_tools.py | 6 ++-- gently/app/tools/memory_tools.py | 2 +- gently/app/tools/resolution_tools.py | 11 +++---- gently/app/tools/session_tools.py | 4 +-- gently/app/tools/stage_tools.py | 10 +++--- gently/app/tools/temperature_tools.py | 5 +-- gently/app/tools/timelapse_tools.py | 5 +-- gently/app/tools/volume_tools.py | 8 ++--- gently/harness/tools/helpers.py | 44 +++++++++++++++++++------- pyproject.toml | 16 ---------- 15 files changed, 91 insertions(+), 81 deletions(-) diff --git a/gently/app/tools/acquisition_tools.py b/gently/app/tools/acquisition_tools.py index 83a2deae..8efcef79 100644 --- a/gently/app/tools/acquisition_tools.py +++ b/gently/app/tools/acquisition_tools.py @@ -9,7 +9,7 @@ import numpy as np -from gently.harness.tools.helpers import get_embryo_or_error +from gently.harness.tools.helpers import ctx_get, get_embryo_or_error from gently.harness.tools.registry import ToolCategory, ToolExample, tool logger = logging.getLogger(__name__) @@ -48,8 +48,8 @@ async def acquire_volume( context: dict | None = None, ) -> str: """Acquire single volume - moves to embryo first, uses calibration""" - agent = context.get("agent") - client = context.get("client") + agent = ctx_get(context, "agent") + client = ctx_get(context, "client") if not agent: return "Error: No agent context" @@ -244,8 +244,8 @@ async def capture_lightsheet( context: dict | None = None, ) -> str: """Capture and optionally display a single lightsheet image""" - client = context.get("client") - agent = context.get("agent") + client = ctx_get(context, "client") + agent = ctx_get(context, "agent") try: embryo = None @@ -357,8 +357,8 @@ async def capture_lightsheet( ) async def batch_lightsheet(galvo_position: float = 0.0, context: dict | None = None) -> str: """Capture lightsheet images from all embryos and show them in the web UI""" - agent = context.get("agent") - client = context.get("client") + agent = ctx_get(context, "agent") + client = ctx_get(context, "client") if not agent or not client: return "Error: Agent or microscope not available" diff --git a/gently/app/tools/calibration_tools.py b/gently/app/tools/calibration_tools.py index 5769cee1..3db84e79 100644 --- a/gently/app/tools/calibration_tools.py +++ b/gently/app/tools/calibration_tools.py @@ -14,7 +14,7 @@ from gently.analysis.core import AdaptiveSweepState, FitFunction, fit_focus_curve # noqa: E402 from gently.harness.state import CalibrationPrior # noqa: E402 -from gently.harness.tools.helpers import get_embryo_or_error # noqa: E402 +from gently.harness.tools.helpers import ctx_get, get_embryo_or_error # noqa: E402 from gently.harness.tools.registry import ToolCategory, ToolExample, tool # noqa: E402 from gently.ui.web.plots import ( # noqa: E402 generate_calibration_summary_plot, @@ -462,7 +462,7 @@ async def hybrid_focus_selection( agent, embryo_id: str, fft_confidence_threshold: float = 0.85, -) -> tuple[int, str, float]: +) -> tuple[int, str, float | None]: """ Two-stage focus selection: FFT first, Vision if ambiguous. @@ -696,8 +696,8 @@ async def fast_calibrate_embryo( """ from gently.hardware.dispim.claude_client import AsyncClaudeClient - agent = context.get("agent") - client = context.get("client") + agent = ctx_get(context, "agent") + client = ctx_get(context, "client") if not agent: return False, "Error: No agent context", 0 @@ -999,8 +999,8 @@ async def calibrate_embryo( from gently.analysis.core import calculate_focus_score from gently.hardware.dispim.claude_client import AsyncClaudeClient - agent = context.get("agent") - client = context.get("client") + agent = ctx_get(context, "agent") + client = ctx_get(context, "client") if not agent: return "Error: No agent context" @@ -1402,7 +1402,7 @@ async def calibrate_all_embryos( context: dict | None = None, ) -> str: """Calibrate all embryos sequentially with Claude vision""" - agent = context.get("agent") + agent = ctx_get(context, "agent") if not agent: return "Error: No agent context" diff --git a/gently/app/tools/detection_tools.py b/gently/app/tools/detection_tools.py index b821ec72..fd7d826b 100644 --- a/gently/app/tools/detection_tools.py +++ b/gently/app/tools/detection_tools.py @@ -21,6 +21,7 @@ pixel_to_stage_position, stage_to_pixel_position, ) +from gently.harness.tools.helpers import ctx_get from gently.harness.tools.registry import ToolCategory, ToolExample, tool @@ -48,7 +49,7 @@ async def _route_to_map_view( marked = await mark_embryos_web( viz_server=agent.viz_server, image=image, - initial_stage_position=tuple(stage_position), + initial_stage_position=stage_position, pixel_size_um=pixel_size_um, initial_markers=initial_markers, default_role=default_role, @@ -121,8 +122,8 @@ async def detect_embryos( context: dict | None = None, ) -> str: """Detect embryos via SAM + edit/assign roles in the web map view.""" - agent = context.get("agent") - client = context.get("client") + agent = ctx_get(context, "agent") + client = ctx_get(context, "client") if not agent: return "Error: No agent context" @@ -240,7 +241,7 @@ async def detect_embryos( except Exception: pass - role_counts = {} + role_counts: dict[str, int] = {} for _, r in added: role_counts[r] = role_counts.get(r, 0) + 1 role_summary = ", ".join(f"{n} {r}" for r, n in sorted(role_counts.items())) @@ -281,8 +282,8 @@ async def manual_mark_embryos( context: dict | None = None, ) -> str: """Manual marking via the web map view.""" - agent = context.get("agent") - client = context.get("client") + agent = ctx_get(context, "agent") + client = ctx_get(context, "client") if not agent: return "Error: No agent context" @@ -420,7 +421,7 @@ async def edit_embryos( context: dict | None = None, ) -> str: """Edit existing embryos via the web map view.""" - agent = context.get("agent") + agent = ctx_get(context, "agent") if not agent: return "Error: No agent context" if not agent.experiment.embryos: @@ -452,8 +453,8 @@ async def edit_embryos( ) async def show_detected_embryos(save_to_file: bool = True, context: dict | None = None) -> str: """Show detected embryos visualization using experiment.embryos as source of truth""" - agent = context.get("agent") - client = context.get("client") + agent = ctx_get(context, "agent") + client = ctx_get(context, "client") if not agent: return "Error: No agent context" diff --git a/gently/app/tools/focus_tools.py b/gently/app/tools/focus_tools.py index cef6ca81..11769eb4 100644 --- a/gently/app/tools/focus_tools.py +++ b/gently/app/tools/focus_tools.py @@ -22,6 +22,7 @@ calculate_focus_score, fit_focus_curve, ) +from gently.harness.tools.helpers import ctx_get from gently.harness.tools.registry import ToolCategory, ToolExample, tool @@ -82,8 +83,8 @@ async def fine_focus( context : dict Execution context with client and agent """ - client = context.get("client") - agent = context.get("agent") + client = ctx_get(context, "client") + agent = ctx_get(context, "agent") if not client: return "Error: No microscope client connected" @@ -253,7 +254,7 @@ async def get_focus_score( context : dict Execution context """ - client = context.get("client") + client = ctx_get(context, "client") if not client: return "Error: No microscope client connected" @@ -315,7 +316,7 @@ async def get_focus_history(embryo_id: str, context: dict | None = None) -> str: context : dict Execution context with agent """ - agent = context.get("agent") + agent = ctx_get(context, "agent") if not agent: return "Error: No agent context available" diff --git a/gently/app/tools/led_tools.py b/gently/app/tools/led_tools.py index 92aac49c..df02a8ae 100644 --- a/gently/app/tools/led_tools.py +++ b/gently/app/tools/led_tools.py @@ -4,6 +4,7 @@ Tools for controlling microscope LED illumination. """ +from gently.harness.tools.helpers import ctx_get from gently.harness.tools.registry import ToolCategory, tool @@ -15,7 +16,7 @@ ) async def set_led(state: str, context: dict) -> str: """Set LED state""" - client = context.get("client") + client = ctx_get(context, "client") try: result = await client.set_led(state) @@ -35,7 +36,7 @@ async def set_led(state: str, context: dict) -> str: ) async def get_led_status(context: dict) -> str: """Get LED status""" - client = context.get("client") + client = ctx_get(context, "client") try: result = await client.get_led_status() diff --git a/gently/app/tools/light_source_tools.py b/gently/app/tools/light_source_tools.py index 065b981f..a190b929 100644 --- a/gently/app/tools/light_source_tools.py +++ b/gently/app/tools/light_source_tools.py @@ -14,7 +14,7 @@ ``modify_parameters(embryo_id, {"laser_power_488_pct": ...}, ...)``. """ -from gently.harness.tools.helpers import require_agent +from gently.harness.tools.helpers import ctx_get, require_agent from gently.harness.tools.registry import ToolCategory, ToolExample, tool @@ -49,7 +49,7 @@ async def set_laser_power( agent, err = require_agent(context) if err: return err - client = context.get("client") + client = ctx_get(context, "client") if not client: return "Error: Microscope not connected." @@ -93,7 +93,7 @@ async def get_laser_power( agent, err = require_agent(context) if err: return err - client = context.get("client") + client = ctx_get(context, "client") if not client: return "Error: Microscope not connected." diff --git a/gently/app/tools/memory_tools.py b/gently/app/tools/memory_tools.py index 0c0306e2..d58d3e5d 100644 --- a/gently/app/tools/memory_tools.py +++ b/gently/app/tools/memory_tools.py @@ -7,7 +7,7 @@ from gently.harness.tools.registry import ToolCategory, ToolExample, tool -def _get_memory(context: dict): +def _get_memory(context: dict | None): """Extract AgentMemory from tool context.""" agent = context.get("agent") if context else None if not agent or not hasattr(agent, "memory") or not agent.memory: diff --git a/gently/app/tools/resolution_tools.py b/gently/app/tools/resolution_tools.py index f5a6c19f..c3a152da 100644 --- a/gently/app/tools/resolution_tools.py +++ b/gently/app/tools/resolution_tools.py @@ -20,6 +20,8 @@ """ import logging +from types import SimpleNamespace +from typing import Any from gently.harness.tools.helpers import require_agent from gently.harness.tools.registry import ToolCategory, ToolExample, tool @@ -389,10 +391,7 @@ async def apply_plan_acquisition_spec( for embryo in experiment.embryos.values(): # Build an "effective" spec respecting overrides — copy then # zero out any field the caller asked us to skip. - class _Filtered: - pass - - eff = _Filtered() + eff = SimpleNamespace() eff.num_slices = ( None if "num_slices" in overrides and overrides["num_slices"] is None @@ -554,10 +553,10 @@ async def recall_sibling_sessions( sid = getattr(item, "session_id", None) if not sid: continue - meta = {} + meta: dict[str, Any] = {} if file_store is not None: try: - meta = file_store.get_session(sid) or {} + meta = dict(file_store.get_session(sid) or {}) except Exception: meta = {} sessions.append( diff --git a/gently/app/tools/session_tools.py b/gently/app/tools/session_tools.py index bfe79988..5490c8bc 100644 --- a/gently/app/tools/session_tools.py +++ b/gently/app/tools/session_tools.py @@ -262,8 +262,8 @@ def analyze_corrections(limit: int = 50, context: dict | None = None) -> str: "", ] - indicator_counts = {} - tool_corrections = {} + indicator_counts: dict[str, int] = {} + tool_corrections: dict[str, int] = {} for corr in corrections[:limit]: for indicator in corr.correction_indicators: diff --git a/gently/app/tools/stage_tools.py b/gently/app/tools/stage_tools.py index 39f433f3..c4e9ab55 100644 --- a/gently/app/tools/stage_tools.py +++ b/gently/app/tools/stage_tools.py @@ -4,7 +4,7 @@ Tools for controlling microscope XY stage movement. """ -from gently.harness.tools.helpers import get_embryo_or_error +from gently.harness.tools.helpers import ctx_get, get_embryo_or_error from gently.harness.tools.registry import ToolCategory, ToolExample, tool @@ -24,8 +24,8 @@ ) async def move_to_embryo(embryo_id: str, context: dict) -> str: """Move stage to embryo position""" - agent = context.get("agent") - client = context.get("client") + agent = ctx_get(context, "agent") + client = ctx_get(context, "client") if not agent: return "Error: No agent context" @@ -66,7 +66,7 @@ async def move_to_embryo(embryo_id: str, context: dict) -> str: ) async def get_stage_position(context: dict) -> str: """Get current stage position""" - client = context.get("client") + client = ctx_get(context, "client") if not client: return "Error: No microscope client connected" @@ -94,7 +94,7 @@ async def get_stage_position(context: dict) -> str: ) async def move_stage(x: float, y: float, context: dict | None = None) -> str: """Move stage to arbitrary XY coordinates""" - client = context.get("client") + client = ctx_get(context, "client") if not client: return "Error: No microscope client connected" diff --git a/gently/app/tools/temperature_tools.py b/gently/app/tools/temperature_tools.py index 6e12e7e8..d9fd303c 100644 --- a/gently/app/tools/temperature_tools.py +++ b/gently/app/tools/temperature_tools.py @@ -6,6 +6,7 @@ part of closed-loop experiments. """ +from gently.harness.tools.helpers import ctx_get from gently.harness.tools.registry import ToolCategory, ToolExample, tool @@ -33,7 +34,7 @@ async def set_temperature(target_c: float, context: dict) -> str: target_c : float Target temperature in degrees Celsius (0.0-99.9). """ - client = context.get("client") + client = ctx_get(context, "client") try: result = await client.set_temperature(target_c) if result.get("success"): @@ -62,7 +63,7 @@ async def set_temperature(target_c: float, context: dict) -> str: ) async def get_temperature(context: dict) -> str: """Read current temperature, setpoint, and lock state.""" - client = context.get("client") + client = ctx_get(context, "client") try: r = await client.get_temperature() if r.get("success"): diff --git a/gently/app/tools/timelapse_tools.py b/gently/app/tools/timelapse_tools.py index c794b9ac..d5ec3d76 100644 --- a/gently/app/tools/timelapse_tools.py +++ b/gently/app/tools/timelapse_tools.py @@ -5,6 +5,7 @@ """ from gently.harness.tools.helpers import ( + ctx_get, get_embryo_or_error, require_agent, require_developmental_tracker, @@ -26,7 +27,7 @@ async def generate_bluesky_plan( context: dict | None = None, ) -> str: """Generate Bluesky plan""" - agent = context.get("agent") + agent = ctx_get(context, "agent") if not agent: return "Error: No agent context" @@ -966,7 +967,7 @@ def get_photodose_status(context: dict | None = None) -> str: if err: return err base = getattr(orchestrator, "_dose_budget_base_ms", None) - exceeded = getattr(orchestrator, "_dose_budget_exceeded", set()) or set() + exceeded: set[str] = getattr(orchestrator, "_dose_budget_exceeded", set()) or set() states = getattr(orchestrator, "_embryo_states", {}) or {} if base is None: lines = ["Photodose budget: DISABLED (no cap).", ""] diff --git a/gently/app/tools/volume_tools.py b/gently/app/tools/volume_tools.py index 02f1afd0..dd05cec5 100644 --- a/gently/app/tools/volume_tools.py +++ b/gently/app/tools/volume_tools.py @@ -7,7 +7,7 @@ import logging from gently.core.coordinates import get_um_per_pixel, stage_to_pixel_position -from gently.harness.tools.helpers import require_agent +from gently.harness.tools.helpers import ctx_get, require_agent from gently.harness.tools.registry import ToolCategory, ToolExample, tool logger = logging.getLogger(__name__) @@ -36,8 +36,8 @@ async def view_image( context: dict | None = None, ) -> str: """Capture and display bottom camera image with embryo annotations""" - client = context.get("client") - agent = context.get("agent") + client = ctx_get(context, "client") + agent = ctx_get(context, "agent") try: snap = await client.capture_bottom_image(exposure_ms=exposure_ms) @@ -252,7 +252,7 @@ async def list_volumes(embryo_id: str | None = None, context: dict | None = None all_volumes_list = agent.store.list_volumes(session_id, embryo_id) # Group by embryo_id - all_volumes = {} # embryo_id -> list of volume records + all_volumes: dict[str, list[dict]] = {} # embryo_id -> list of volume records for vol in all_volumes_list: eid = vol["embryo_id"] if eid not in all_volumes: diff --git a/gently/harness/tools/helpers.py b/gently/harness/tools/helpers.py index fa98cf62..b4c58f39 100644 --- a/gently/harness/tools/helpers.py +++ b/gently/harness/tools/helpers.py @@ -9,13 +9,35 @@ from typing import Any -def require_agent(context: dict) -> tuple[Any | None, str | None]: +def ctx_get(context: dict | None, key: str) -> Any: + """ + Look up a key in a (possibly missing) tool execution context + + Parameters + ---------- + context : dict | None + Tool execution context + key : str + Key to look up + + Returns + ------- + Any + The value for ``key``, or ``None`` if ``context`` is ``None`` or + the key is absent. + """ + if context is None: + return None + return context.get(key) + + +def require_agent(context: dict | None) -> tuple[Any, str | None]: """ Extract agent from context or return error message Parameters ---------- - context : dict + context : dict | None Tool execution context Returns @@ -23,13 +45,13 @@ def require_agent(context: dict) -> tuple[Any | None, str | None]: tuple (agent, None) if found, (None, error_message) if not """ - agent = context.get("agent") + agent = ctx_get(context, "agent") if not agent: return None, "Error: No agent context" return agent, None -def get_embryo_or_error(agent, embryo_id: str) -> tuple[Any | None, str | None]: +def get_embryo_or_error(agent, embryo_id: str) -> tuple[Any, str | None]: """ Get embryo by any name or return error message @@ -51,13 +73,13 @@ def get_embryo_or_error(agent, embryo_id: str) -> tuple[Any | None, str | None]: return embryo, None -def require_microscope(context: dict) -> tuple[Any | None, str | None]: +def require_microscope(context: dict | None) -> tuple[Any, str | None]: """ Get microscope client from context or return error message Parameters ---------- - context : dict + context : dict | None Tool execution context Returns @@ -65,13 +87,13 @@ def require_microscope(context: dict) -> tuple[Any | None, str | None]: tuple (client, None) if connected, (None, error_message) if not """ - client = context.get("client") + client = ctx_get(context, "client") if not client: return None, "Not connected to microscope. Use connect_microscope first." return client, None -def require_interaction_logger(agent) -> tuple[Any | None, str | None]: +def require_interaction_logger(agent) -> tuple[Any, str | None]: """ Get interaction logger or return error message @@ -90,7 +112,7 @@ def require_interaction_logger(agent) -> tuple[Any | None, str | None]: return agent.interaction_logger, None -def require_developmental_tracker(agent) -> tuple[Any | None, str | None]: +def require_developmental_tracker(agent) -> tuple[Any, str | None]: """ Get developmental tracker or return error message @@ -112,7 +134,7 @@ def require_developmental_tracker(agent) -> tuple[Any | None, str | None]: return agent.developmental_tracker, None -def require_timelapse_orchestrator(agent) -> tuple[Any | None, str | None]: +def require_timelapse_orchestrator(agent) -> tuple[Any, str | None]: """ Get timelapse orchestrator or return error message @@ -131,7 +153,7 @@ def require_timelapse_orchestrator(agent) -> tuple[Any | None, str | None]: return agent.timelapse_orchestrator, None -def require_databroker(agent) -> tuple[Any | None, str | None]: +def require_databroker(agent) -> tuple[Any, str | None]: """ Get databroker connection or return error message diff --git a/pyproject.toml b/pyproject.toml index 8f00eacf..4b5c78f1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -176,22 +176,6 @@ module = [ "gently.app.calibration.edge_roi", "gently.app.detectors.dopaminergic_signal", "gently.app.orchestration.timelapse", - "gently.app.tools.acquisition_tools", - "gently.app.tools.analysis_tools", - "gently.app.tools.calibration_tools", - "gently.app.tools.detection_tools", - "gently.app.tools.experiment_tools", - "gently.app.tools.focus_tools", - "gently.app.tools.led_tools", - "gently.app.tools.light_source_tools", - "gently.app.tools.memory_tools", - "gently.app.tools.plan_execution_tools", - "gently.app.tools.resolution_tools", - "gently.app.tools.session_tools", - "gently.app.tools.stage_tools", - "gently.app.tools.temperature_tools", - "gently.app.tools.timelapse_tools", - "gently.app.tools.volume_tools", "gently.app.video_maker", "gently.core.event_bus", "gently.core.file_store", From b6d9e598d0805fe0f7233a82ef14bf622cf8aad1 Mon Sep 17 00:00:00 2001 From: subindevs <36504048+subindevs@users.noreply.github.com> Date: Mon, 15 Jun 2026 01:08:10 +0100 Subject: [PATCH 7/8] Address PR #48 review findings: fix view_image None-client crash and remaining type gaps - volume_tools.view_image: guard against a disconnected microscope client via require_microscope() instead of crashing on client.capture_bottom_image() - file_store/store register_volume: volume_data: np.ndarray = None -> np.ndarray | None = None (implicit-Optional missed in the earlier pass) - StoreProtocol: mark @runtime_checkable for consistency with gently.harness.protocols - TimelapseOrchestrator.start: embryo_ids: list[str] -> list[str] | None = None to match its actual None-handling and the docstring Co-Authored-By: Claude Sonnet 4.6 --- gently/app/orchestration/timelapse.py | 2 +- gently/app/tools/volume_tools.py | 6 ++++-- gently/core/file_store.py | 2 +- gently/core/store.py | 2 +- gently/harness/memory/_protocols.py | 3 ++- 5 files changed, 9 insertions(+), 6 deletions(-) diff --git a/gently/app/orchestration/timelapse.py b/gently/app/orchestration/timelapse.py index 5d0783e4..f4a2692a 100644 --- a/gently/app/orchestration/timelapse.py +++ b/gently/app/orchestration/timelapse.py @@ -195,7 +195,7 @@ def __init__( async def start( self, - embryo_ids: list[str], + embryo_ids: list[str] | None = None, stop_condition: str = "manual", base_interval_seconds: float = 120.0, condition_value: Any = None, diff --git a/gently/app/tools/volume_tools.py b/gently/app/tools/volume_tools.py index dd05cec5..3a9d3d5c 100644 --- a/gently/app/tools/volume_tools.py +++ b/gently/app/tools/volume_tools.py @@ -7,7 +7,7 @@ import logging from gently.core.coordinates import get_um_per_pixel, stage_to_pixel_position -from gently.harness.tools.helpers import ctx_get, require_agent +from gently.harness.tools.helpers import ctx_get, require_agent, require_microscope from gently.harness.tools.registry import ToolCategory, ToolExample, tool logger = logging.getLogger(__name__) @@ -36,7 +36,9 @@ async def view_image( context: dict | None = None, ) -> str: """Capture and display bottom camera image with embryo annotations""" - client = ctx_get(context, "client") + client, err = require_microscope(context) + if err: + return err agent = ctx_get(context, "agent") try: diff --git a/gently/core/file_store.py b/gently/core/file_store.py index 8cd23095..5a8b7f01 100644 --- a/gently/core/file_store.py +++ b/gently/core/file_store.py @@ -656,7 +656,7 @@ def register_volume( timepoint: int, incoming_path: Path, metadata: dict | None = None, - volume_data: np.ndarray = None, + volume_data: np.ndarray | None = None, ) -> Path: """ Zero-copy path: move an existing TIFF to its canonical location. diff --git a/gently/core/store.py b/gently/core/store.py index d33764da..6a60a202 100644 --- a/gently/core/store.py +++ b/gently/core/store.py @@ -492,7 +492,7 @@ def register_volume( timepoint: int, incoming_path: Path, metadata: dict | None = None, - volume_data: np.ndarray = None, + volume_data: np.ndarray | None = None, ) -> Path: """ Zero-copy path: rename an existing TIFF to canonical location. diff --git a/gently/harness/memory/_protocols.py b/gently/harness/memory/_protocols.py index 7fddeb77..16162a29 100644 --- a/gently/harness/memory/_protocols.py +++ b/gently/harness/memory/_protocols.py @@ -12,11 +12,12 @@ import sqlite3 from contextlib import AbstractContextManager -from typing import Protocol +from typing import Protocol, runtime_checkable from .model import Campaign, PlanItem +@runtime_checkable class StoreProtocol(Protocol): _conn: sqlite3.Connection From 7da8443d5f23ed3a9e5cf08fdd04abf8a7ad9a77 Mon Sep 17 00:00:00 2001 From: P S Kesavan Date: Tue, 16 Jun 2026 04:13:07 +0530 Subject: [PATCH 8/8] mypy: pin to 2.1.0 across CI/pre-commit/dev; fix missed implicit-Optional MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI installed mypy unpinned (`pip install mypy`) while the pre-commit hook pinned mirrors-mypy v2.1.0, so the two would diverge the moment a newer mypy released — "passes my pre-commit" would stop implying "passes CI". Pin all three sources to 2.1.0 (CI install, pyproject dev group, and the pre-commit rev) and cross-reference them so they move together. Also convert the one implicit-Optional missed in Phase 1: `view_image`'s `image: np.ndarray = None` in dispim/client.py (the other three params in that same signature were already converted). Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/lint.yml | 4 +++- .pre-commit-config.yaml | 4 +++- gently/hardware/dispim/client.py | 2 +- pyproject.toml | 5 ++++- 4 files changed, 11 insertions(+), 4 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 58d2538e..d11c7f22 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -25,8 +25,10 @@ jobs: - name: ruff format check run: ruff format --check . + # Pin to match the pre-commit mirrors-mypy rev and the pyproject dev + # group so CI, local commits, and `uv sync` never run different mypys. - name: Install mypy - run: pip install mypy + run: pip install mypy==2.1.0 - name: mypy run: mypy . diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7f6f8bdf..88476e0c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -7,7 +7,9 @@ repos: - id: ruff-format - repo: https://github.com/pre-commit/mirrors-mypy - rev: v2.1.0 # run `pre-commit autoupdate` to pin to latest + # Keep in sync with the mypy pin in pyproject.toml dev group and + # .github/workflows/lint.yml. Run `pre-commit autoupdate` to bump. + rev: v2.1.0 hooks: - id: mypy pass_filenames: false diff --git a/gently/hardware/dispim/client.py b/gently/hardware/dispim/client.py index 9401fd37..2e4567e1 100644 --- a/gently/hardware/dispim/client.py +++ b/gently/hardware/dispim/client.py @@ -1113,7 +1113,7 @@ async def _get_detection_image( async def view_image( self, - image: np.ndarray = None, + image: np.ndarray | None = None, title: str = "Image View", exposure_ms: float | None = None, save_path: str | None = None, diff --git a/pyproject.toml b/pyproject.toml index 4b5c78f1..d2a0c9a4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -94,7 +94,10 @@ dev = [ "pytest-asyncio>=0.21.0", "ruff>=0.4.0", "pre-commit>=3.7.0", - "mypy>=1.8.0", + # Pinned so local runs, the pre-commit hook, and CI all use the same + # mypy. Keep in sync with the mirrors-mypy rev in .pre-commit-config.yaml + # and the install step in .github/workflows/lint.yml. + "mypy==2.1.0", ] [project.scripts]