jupytermind 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/skills/ai-chemistry-scientist/SKILL.md +97 -0
- package/.github/skills/ai-chemistry-scientist/manifest.json +156 -0
- package/.github/skills/ai-data-scientist/SKILL.md +330 -0
- package/.github/skills/ai-genomics-scientist/SKILL.md +98 -0
- package/.github/skills/ai-genomics-scientist/manifest.json +93 -0
- package/.github/skills/ai-materials-scientist/SKILL.md +51 -0
- package/.github/skills/ai-materials-scientist/manifest.json +58 -0
- package/.github/skills/ai-scientist/SKILL.md +69 -0
- package/.github/skills/ai-scientist/manifest.json +61 -0
- package/.github/skills/ai-structural-biology-scientist/SKILL.md +67 -0
- package/.github/skills/ai-structural-biology-scientist/manifest.json +72 -0
- package/.github/skills/japanese-prose/NOTICE.md +17 -0
- package/.github/skills/japanese-prose/SKILL.md +111 -0
- package/.github/skills/japanese-prose/references/review-workflow.md +50 -0
- package/.github/skills/japanese-prose/references/scoring.md +24 -0
- package/.github/skills/japanese-prose/references/writing-guidelines.md +60 -0
- package/.github/skills/japanese-prose/scripts/core.py +192 -0
- package/.github/skills/japanese-prose/scripts/fixtures/natural.md +5 -0
- package/.github/skills/japanese-prose/scripts/fixtures/unnatural.md +5 -0
- package/.github/skills/japanese-prose/scripts/lint.py +378 -0
- package/.github/skills/japanese-prose/scripts/outline.py +68 -0
- package/.github/skills/japanese-prose/scripts/terms.py +112 -0
- package/.github/skills/japanese-prose/scripts/test_engine.py +117 -0
- package/.github/skills/presentation-planner/SKILL.md +257 -0
- package/.github/skills/presentation-planner/assets/design-templates/data-report.yaml +97 -0
- package/.github/skills/presentation-planner/assets/design-templates/executive-proposal.yaml +92 -0
- package/.github/skills/presentation-planner/assets/design-templates/technical-briefing.yaml +96 -0
- package/.github/skills/presentation-planner/assets/scenario-templates/data-report.md +47 -0
- package/.github/skills/presentation-planner/assets/scenario-templates/executive-decision.md +43 -0
- package/.github/skills/presentation-planner/assets/scenario-templates/technical-briefing.md +45 -0
- package/.github/skills/presentation-planner/references/customizing-design-templates.md +160 -0
- package/.github/skills/presentation-planner/references/design-spec-schema.md +72 -0
- package/.github/skills/presentation-planner/references/handoff-contract.md +49 -0
- package/.github/skills/presentation-planner/references/responsibility-boundary.md +32 -0
- package/.github/skills/presentation-planner/references/scenario-templates.md +55 -0
- package/.github/skills/tech-writer/SKILL.md +434 -0
- package/.github/skills/tech-writer/assets/templates/blueprint.md +187 -0
- package/.github/skills/tech-writer/assets/templates/design-doc.md +29 -0
- package/.github/skills/tech-writer/assets/templates/migration-plan.md +173 -0
- package/.github/skills/tech-writer/assets/templates/operations-runbook.md +202 -0
- package/.github/skills/tech-writer/assets/templates/pr-description.md +23 -0
- package/.github/skills/tech-writer/assets/templates/qiita.md +44 -0
- package/.github/skills/tech-writer/assets/templates/readme.md +38 -0
- package/.github/skills/tech-writer/assets/templates/requirements-definition.md +170 -0
- package/.github/skills/tech-writer/assets/templates/rfi.md +113 -0
- package/.github/skills/tech-writer/assets/templates/rfp.md +180 -0
- package/.github/skills/tech-writer/assets/templates/security-design.md +167 -0
- package/.github/skills/tech-writer/assets/templates/system-design.md +220 -0
- package/.github/skills/tech-writer/assets/templates/technical-proposal.md +112 -0
- package/.github/skills/tech-writer/assets/templates/test-plan.md +153 -0
- package/.github/skills/tech-writer/assets/templates/user-manual.md +22 -0
- package/.github/skills/tech-writer/assets/templates/white-paper.md +192 -0
- package/.github/skills/tech-writer/references/doctypes/api-docs.md +33 -0
- package/.github/skills/tech-writer/references/doctypes/blueprint.md +81 -0
- package/.github/skills/tech-writer/references/doctypes/code-comments.md +39 -0
- package/.github/skills/tech-writer/references/doctypes/design-doc.md +42 -0
- package/.github/skills/tech-writer/references/doctypes/migration-plan.md +63 -0
- package/.github/skills/tech-writer/references/doctypes/operations-runbook.md +63 -0
- package/.github/skills/tech-writer/references/doctypes/pr-commit.md +82 -0
- package/.github/skills/tech-writer/references/doctypes/qiita.md +75 -0
- package/.github/skills/tech-writer/references/doctypes/readme.md +43 -0
- package/.github/skills/tech-writer/references/doctypes/release-notes.md +30 -0
- package/.github/skills/tech-writer/references/doctypes/requirements-definition.md +61 -0
- package/.github/skills/tech-writer/references/doctypes/rfi.md +43 -0
- package/.github/skills/tech-writer/references/doctypes/rfp.md +46 -0
- package/.github/skills/tech-writer/references/doctypes/security-design.md +71 -0
- package/.github/skills/tech-writer/references/doctypes/system-design.md +74 -0
- package/.github/skills/tech-writer/references/doctypes/technical-proposal.md +49 -0
- package/.github/skills/tech-writer/references/doctypes/test-plan.md +67 -0
- package/.github/skills/tech-writer/references/doctypes/user-manual.md +58 -0
- package/.github/skills/tech-writer/references/doctypes/white-paper.md +84 -0
- package/.github/skills/tech-writer/references/doctypes/zenn.md +66 -0
- package/.github/skills/tech-writer/references/japanese-prose-optimization.md +110 -0
- package/.github/skills/tech-writer/references/style-constitution.md +104 -0
- package/.github/skills/tech-writer/scripts/lint.py +412 -0
- package/LICENSE +21 -0
- package/README.md +92 -0
- package/bin/ai-data-scientist.js +123 -0
- package/package.json +41 -0
- package/pyproject.toml +45 -0
- package/src/ai_chemistry_scientist/__init__.py +0 -0
- package/src/ai_chemistry_scientist/admet_prediction.py +71 -0
- package/src/ai_chemistry_scientist/bioactivity_classification.py +73 -0
- package/src/ai_chemistry_scientist/data/sample_molecules.csv +21 -0
- package/src/ai_chemistry_scientist/dispatch.py +369 -0
- package/src/ai_chemistry_scientist/docking_score.py +97 -0
- package/src/ai_chemistry_scientist/drug_likeness_rules.py +84 -0
- package/src/ai_chemistry_scientist/evidence.py +41 -0
- package/src/ai_chemistry_scientist/molecular_descriptors.py +97 -0
- package/src/ai_chemistry_scientist/molecular_formula_mass.py +40 -0
- package/src/ai_chemistry_scientist/molecular_similarity.py +78 -0
- package/src/ai_chemistry_scientist/qsar_modeling.py +105 -0
- package/src/ai_chemistry_scientist/salt_standardization.py +81 -0
- package/src/ai_chemistry_scientist/structural_alerts.py +76 -0
- package/src/ai_chemistry_scientist/structure_format_conversion.py +84 -0
- package/src/ai_chemistry_scientist/validation.py +70 -0
- package/src/ai_data_scientist/__init__.py +0 -0
- package/src/ai_data_scientist/analysis_assumptions.py +121 -0
- package/src/ai_data_scientist/anomaly_detection.py +39 -0
- package/src/ai_data_scientist/automl.py +109 -0
- package/src/ai_data_scientist/cleaning.py +56 -0
- package/src/ai_data_scientist/cli.py +90 -0
- package/src/ai_data_scientist/clustering.py +54 -0
- package/src/ai_data_scientist/dashboard.py +33 -0
- package/src/ai_data_scientist/data_definition.py +100 -0
- package/src/ai_data_scientist/data_quality.py +164 -0
- package/src/ai_data_scientist/dataset_validation.py +135 -0
- package/src/ai_data_scientist/dependency_pins.py +60 -0
- package/src/ai_data_scientist/eda.py +82 -0
- package/src/ai_data_scientist/experiment_evaluation.py +635 -0
- package/src/ai_data_scientist/explainability.py +340 -0
- package/src/ai_data_scientist/feature_engineering.py +163 -0
- package/src/ai_data_scientist/gate_config.py +32 -0
- package/src/ai_data_scientist/ingestion.py +127 -0
- package/src/ai_data_scientist/insight_engine.py +180 -0
- package/src/ai_data_scientist/japanese_nlp.py +43 -0
- package/src/ai_data_scientist/jupyter_launcher.py +137 -0
- package/src/ai_data_scientist/jupyter_mcp_client.py +94 -0
- package/src/ai_data_scientist/language_router.py +28 -0
- package/src/ai_data_scientist/lifecycle.py +221 -0
- package/src/ai_data_scientist/mcp_gateway.py +113 -0
- package/src/ai_data_scientist/mcp_runtime.py +194 -0
- package/src/ai_data_scientist/mcp_transport.py +53 -0
- package/src/ai_data_scientist/ml_modeling.py +451 -0
- package/src/ai_data_scientist/model_tuning.py +104 -0
- package/src/ai_data_scientist/notebook_audit.py +574 -0
- package/src/ai_data_scientist/project_manager.py +243 -0
- package/src/ai_data_scientist/report_export.py +73 -0
- package/src/ai_data_scientist/sensitivity.py +445 -0
- package/src/ai_data_scientist/signal_analysis.py +201 -0
- package/src/ai_data_scientist/skill_packaging.py +40 -0
- package/src/ai_data_scientist/stats_analysis.py +88 -0
- package/src/ai_data_scientist/text_nlp.py +44 -0
- package/src/ai_data_scientist/timeseries.py +68 -0
- package/src/ai_data_scientist/visualization.py +708 -0
- package/src/ai_genomics_scientist/__init__.py +1 -0
- package/src/ai_genomics_scientist/differential_expression.py +147 -0
- package/src/ai_genomics_scientist/dispatch.py +267 -0
- package/src/ai_genomics_scientist/evidence.py +45 -0
- package/src/ai_genomics_scientist/gene_set_enrichment.py +76 -0
- package/src/ai_genomics_scientist/sequence_alignment.py +97 -0
- package/src/ai_genomics_scientist/sequence_features.py +111 -0
- package/src/ai_genomics_scientist/splice_site_scoring.py +66 -0
- package/src/ai_genomics_scientist/validation.py +83 -0
- package/src/ai_genomics_scientist/variant_effect.py +147 -0
- package/src/ai_genomics_scientist/variant_pathogenicity.py +125 -0
- package/src/ai_materials_scientist/__init__.py +0 -0
- package/src/ai_materials_scientist/calphad.py +117 -0
- package/src/ai_materials_scientist/classical_monte_carlo.py +165 -0
- package/src/ai_materials_scientist/crystal_plasticity.py +184 -0
- package/src/ai_materials_scientist/dispatch.py +100 -0
- package/src/ai_materials_scientist/evidence.py +84 -0
- package/src/ai_materials_scientist/fem.py +279 -0
- package/src/ai_materials_scientist/kinetic_monte_carlo.py +145 -0
- package/src/ai_materials_scientist/molecular_dynamics.py +240 -0
- package/src/ai_materials_scientist/phase_field.py +167 -0
- package/src/ai_materials_scientist/validation.py +70 -0
- package/src/ai_scientist/__init__.py +1 -0
- package/src/ai_scientist/completion_gate.py +15 -0
- package/src/ai_scientist/data_analysis.py +46 -0
- package/src/ai_scientist/evidence_registry.py +99 -0
- package/src/ai_scientist/experimental_design.py +20 -0
- package/src/ai_scientist/language.py +14 -0
- package/src/ai_scientist/latex_renderer.py +41 -0
- package/src/ai_scientist/literature_review.py +37 -0
- package/src/ai_scientist/manifest.py +87 -0
- package/src/ai_scientist/manuscript.py +94 -0
- package/src/ai_scientist/mcp_config.py +76 -0
- package/src/ai_scientist/mcp_external.py +42 -0
- package/src/ai_scientist/mcp_failures.py +23 -0
- package/src/ai_scientist/mcp_gateway.py +38 -0
- package/src/ai_scientist/mcp_managed.py +180 -0
- package/src/ai_scientist/npm_packaging.py +49 -0
- package/src/ai_scientist/orchestrator.py +133 -0
- package/src/ai_scientist/peer_review.py +60 -0
- package/src/ai_scientist/phase_gate.py +74 -0
- package/src/ai_scientist/phase_state.py +230 -0
- package/src/ai_scientist/presentation.py +56 -0
- package/src/ai_scientist/project_config.py +31 -0
- package/src/ai_scientist/project_handle.py +74 -0
- package/src/ai_scientist/reproducibility.py +20 -0
- package/src/ai_scientist/research_planning.py +20 -0
- package/src/ai_scientist/skill_invocation.py +21 -0
- package/src/ai_scientist/tdd_gate.py +99 -0
- package/src/ai_structural_biology_scientist/__init__.py +0 -0
- package/src/ai_structural_biology_scientist/contact_map.py +87 -0
- package/src/ai_structural_biology_scientist/dispatch.py +269 -0
- package/src/ai_structural_biology_scientist/evidence.py +43 -0
- package/src/ai_structural_biology_scientist/hydrophobicity.py +101 -0
- package/src/ai_structural_biology_scientist/protein_docking_score.py +104 -0
- package/src/ai_structural_biology_scientist/secondary_structure.py +95 -0
- package/src/ai_structural_biology_scientist/structural_similarity.py +74 -0
- package/src/ai_structural_biology_scientist/validation.py +100 -0
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
"""Cooperative run cancellation and quiescence lifecycle API.
|
|
2
|
+
|
|
3
|
+
Implements DES-AIDS-039 (REQ-AIDS-051): a process-wide, thread-safe
|
|
4
|
+
registry of run state that lets an external orchestrator request
|
|
5
|
+
cooperative cancellation of a tracked run and wait until all tracked
|
|
6
|
+
activity (cell executions, notebook writes, locks) has settled.
|
|
7
|
+
|
|
8
|
+
Cancellation is cooperative only: ``request_cancel`` sets a flag that
|
|
9
|
+
``is_cancel_requested`` exposes for callers (such as
|
|
10
|
+
``mcp_gateway.execute_cell``) to check *before* starting new work. It
|
|
11
|
+
cannot interrupt a kernel cell execution already in flight.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import threading
|
|
17
|
+
import time
|
|
18
|
+
from dataclasses import dataclass
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
_VALID_STATES = frozenset({"running", "cancelling", "cancelled", "completed", "failed"})
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class _RunRecord:
|
|
26
|
+
notebook_path: Path
|
|
27
|
+
state: str = "running"
|
|
28
|
+
active_cell_executions: int = 0
|
|
29
|
+
pending_notebook_writes: int = 0
|
|
30
|
+
reason: str | None = None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(frozen=True)
|
|
34
|
+
class RunStatus:
|
|
35
|
+
"""Immutable snapshot of a run's tracked lifecycle state."""
|
|
36
|
+
|
|
37
|
+
run_id: str
|
|
38
|
+
state: str
|
|
39
|
+
active_cell_executions: int
|
|
40
|
+
pending_notebook_writes: int
|
|
41
|
+
locks_held: int
|
|
42
|
+
notebook_path: str
|
|
43
|
+
reason: str | None = None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class UnknownRunError(KeyError):
|
|
47
|
+
"""Raised when a run_id was never registered via register_run."""
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
_registry: dict[str, _RunRecord] = {}
|
|
51
|
+
_registry_guard = threading.Lock()
|
|
52
|
+
_QUIESCENCE_POLL_INTERVAL_S = 0.01
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _get_record(run_id: str) -> _RunRecord:
|
|
56
|
+
with _registry_guard:
|
|
57
|
+
record = _registry.get(run_id)
|
|
58
|
+
if record is None:
|
|
59
|
+
raise UnknownRunError(f"No run registered with run_id={run_id!r}.")
|
|
60
|
+
return record
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _locks_held_for(notebook_path: Path) -> int:
|
|
64
|
+
# project_manager keeps one threading.Lock per notebook path; a lock is
|
|
65
|
+
# "held" iff it currently cannot be acquired without blocking.
|
|
66
|
+
from ai_data_scientist import project_manager
|
|
67
|
+
|
|
68
|
+
lock = project_manager._write_locks.get(notebook_path)
|
|
69
|
+
if lock is None:
|
|
70
|
+
return 0
|
|
71
|
+
acquired = lock.acquire(blocking=False)
|
|
72
|
+
if acquired:
|
|
73
|
+
lock.release()
|
|
74
|
+
return 0
|
|
75
|
+
return 1
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
# @id CODE-AIDS-060
|
|
79
|
+
# @implements REQ-AIDS-051
|
|
80
|
+
# @design DES-AIDS-039
|
|
81
|
+
def register_run(run_id: str, notebook_path: Path | str) -> None:
|
|
82
|
+
"""Register a new tracked run in state "running"."""
|
|
83
|
+
with _registry_guard:
|
|
84
|
+
_registry[run_id] = _RunRecord(notebook_path=Path(notebook_path))
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
# @id CODE-AIDS-061
|
|
88
|
+
# @implements REQ-AIDS-051
|
|
89
|
+
# @design DES-AIDS-039
|
|
90
|
+
def mark_execution_start(run_id: str) -> None:
|
|
91
|
+
"""Record that a cell execution has begun for ``run_id``."""
|
|
92
|
+
record = _get_record(run_id)
|
|
93
|
+
with _registry_guard:
|
|
94
|
+
record.active_cell_executions += 1
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
# @id CODE-AIDS-062
|
|
98
|
+
# @implements REQ-AIDS-051
|
|
99
|
+
# @design DES-AIDS-039
|
|
100
|
+
def mark_execution_end(run_id: str) -> None:
|
|
101
|
+
"""Record that a cell execution has finished for ``run_id``."""
|
|
102
|
+
record = _get_record(run_id)
|
|
103
|
+
with _registry_guard:
|
|
104
|
+
record.active_cell_executions = max(0, record.active_cell_executions - 1)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
# @id CODE-AIDS-063
|
|
108
|
+
# @implements REQ-AIDS-051
|
|
109
|
+
# @design DES-AIDS-039
|
|
110
|
+
def mark_write_start(run_id: str) -> None:
|
|
111
|
+
"""Record that a notebook write has begun for ``run_id``."""
|
|
112
|
+
record = _get_record(run_id)
|
|
113
|
+
with _registry_guard:
|
|
114
|
+
record.pending_notebook_writes += 1
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
# @id CODE-AIDS-064
|
|
118
|
+
# @implements REQ-AIDS-051
|
|
119
|
+
# @design DES-AIDS-039
|
|
120
|
+
def mark_write_end(run_id: str) -> None:
|
|
121
|
+
"""Record that a notebook write has finished for ``run_id``."""
|
|
122
|
+
record = _get_record(run_id)
|
|
123
|
+
with _registry_guard:
|
|
124
|
+
record.pending_notebook_writes = max(0, record.pending_notebook_writes - 1)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
# @id CODE-AIDS-065
|
|
128
|
+
# @implements REQ-AIDS-051
|
|
129
|
+
# @design DES-AIDS-039
|
|
130
|
+
def request_cancel(run_id: str, reason: str | None = None) -> None:
|
|
131
|
+
"""Request cooperative cancellation of ``run_id``.
|
|
132
|
+
|
|
133
|
+
Idempotent and safe to call on an unknown, already-cancelled, or
|
|
134
|
+
already-completed/failed run_id: it never raises.
|
|
135
|
+
"""
|
|
136
|
+
with _registry_guard:
|
|
137
|
+
record = _registry.get(run_id)
|
|
138
|
+
if record is None:
|
|
139
|
+
return
|
|
140
|
+
if record.state == "running":
|
|
141
|
+
record.state = "cancelling"
|
|
142
|
+
record.reason = reason
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
# @id CODE-AIDS-066
|
|
146
|
+
# @implements REQ-AIDS-051
|
|
147
|
+
# @design DES-AIDS-039
|
|
148
|
+
def is_cancel_requested(run_id: str) -> bool:
|
|
149
|
+
"""``True`` iff ``request_cancel`` has been called for ``run_id``."""
|
|
150
|
+
record = _get_record(run_id)
|
|
151
|
+
return record.state in ("cancelling", "cancelled")
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _finalize(run_id: str, state: str) -> None:
|
|
155
|
+
record = _get_record(run_id)
|
|
156
|
+
with _registry_guard:
|
|
157
|
+
record.state = state
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
# @id CODE-AIDS-067
|
|
161
|
+
# @implements REQ-AIDS-051
|
|
162
|
+
# @design DES-AIDS-039
|
|
163
|
+
def mark_completed(run_id: str) -> None:
|
|
164
|
+
"""Mark ``run_id`` completed, unless it was already cancelling/cancelled."""
|
|
165
|
+
record = _get_record(run_id)
|
|
166
|
+
with _registry_guard:
|
|
167
|
+
if record.state in ("cancelling", "cancelled"):
|
|
168
|
+
record.state = "cancelled"
|
|
169
|
+
else:
|
|
170
|
+
record.state = "completed"
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
# @id CODE-AIDS-068
|
|
174
|
+
# @implements REQ-AIDS-051
|
|
175
|
+
# @design DES-AIDS-039
|
|
176
|
+
def mark_failed(run_id: str) -> None:
|
|
177
|
+
"""Mark ``run_id`` failed."""
|
|
178
|
+
_finalize(run_id, "failed")
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
# @id CODE-AIDS-069
|
|
182
|
+
# @implements REQ-AIDS-051
|
|
183
|
+
# @design DES-AIDS-039
|
|
184
|
+
def get_run_status(run_id: str) -> RunStatus:
|
|
185
|
+
"""Return a snapshot of ``run_id``'s current tracked lifecycle state."""
|
|
186
|
+
record = _get_record(run_id)
|
|
187
|
+
with _registry_guard:
|
|
188
|
+
return RunStatus(
|
|
189
|
+
run_id=run_id,
|
|
190
|
+
state=record.state,
|
|
191
|
+
active_cell_executions=record.active_cell_executions,
|
|
192
|
+
pending_notebook_writes=record.pending_notebook_writes,
|
|
193
|
+
locks_held=_locks_held_for(record.notebook_path),
|
|
194
|
+
notebook_path=str(record.notebook_path),
|
|
195
|
+
reason=record.reason,
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
# @id CODE-AIDS-070
|
|
200
|
+
# @implements REQ-AIDS-051
|
|
201
|
+
# @design DES-AIDS-039
|
|
202
|
+
def wait_for_quiescence(run_id: str, timeout_s: float = 30.0) -> RunStatus:
|
|
203
|
+
"""Poll ``run_id`` until all tracked activity settles or ``timeout_s`` elapses.
|
|
204
|
+
|
|
205
|
+
Quiescent means ``active_cell_executions == 0``,
|
|
206
|
+
``pending_notebook_writes == 0`` and ``locks_held == 0``. Returns the
|
|
207
|
+
last observed status either way (callers inspect the returned fields to
|
|
208
|
+
tell quiescence from a timeout).
|
|
209
|
+
"""
|
|
210
|
+
deadline = time.monotonic() + timeout_s
|
|
211
|
+
status = get_run_status(run_id)
|
|
212
|
+
while (
|
|
213
|
+
status.active_cell_executions > 0
|
|
214
|
+
or status.pending_notebook_writes > 0
|
|
215
|
+
or status.locks_held > 0
|
|
216
|
+
):
|
|
217
|
+
if time.monotonic() >= deadline:
|
|
218
|
+
return status
|
|
219
|
+
time.sleep(_QUIESCENCE_POLL_INTERVAL_S)
|
|
220
|
+
status = get_run_status(run_id)
|
|
221
|
+
return status
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""Jupyter MCP execution gateway.
|
|
2
|
+
|
|
3
|
+
Implements DES-AIDS-004: routes all analysis code execution exclusively
|
|
4
|
+
through the configured Jupyter MCP client, enforces execution timeouts,
|
|
5
|
+
and classifies unavailability/timeout failures so no partial cell is ever
|
|
6
|
+
persisted to the notebook.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import concurrent.futures
|
|
12
|
+
from typing import Protocol
|
|
13
|
+
|
|
14
|
+
import nbformat
|
|
15
|
+
|
|
16
|
+
from ai_data_scientist.project_manager import ProjectHandle, enqueue_write, next_execution_count
|
|
17
|
+
|
|
18
|
+
DEFAULT_TIMEOUT_MS = 30000
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class MCPUnavailableError(RuntimeError):
|
|
22
|
+
"""The configured Jupyter MCP server or kernel could not be reached."""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class MCPExecutionTimeoutError(RuntimeError):
|
|
26
|
+
"""Execution did not complete within the configured timeout."""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class MCPClient(Protocol):
|
|
30
|
+
"""Minimal contract any Jupyter MCP transport must satisfy."""
|
|
31
|
+
|
|
32
|
+
def execute(self, code: str) -> dict: ...
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# @id CODE-AIDS-003
|
|
36
|
+
# @implements REQ-AIDS-003
|
|
37
|
+
# @design DES-AIDS-004
|
|
38
|
+
# @id CODE-AIDS-031
|
|
39
|
+
# @implements REQ-AIDS-031
|
|
40
|
+
# @design DES-AIDS-004
|
|
41
|
+
# @id CODE-AIDS-048
|
|
42
|
+
# @implements REQ-AIDS-042
|
|
43
|
+
# @design DES-AIDS-030
|
|
44
|
+
def execute_cell(client: MCPClient, code: str, timeout_ms: int = DEFAULT_TIMEOUT_MS) -> dict:
|
|
45
|
+
"""Execute ``code`` exclusively through ``client`` with a hard timeout.
|
|
46
|
+
|
|
47
|
+
Raises ``MCPExecutionTimeoutError`` if execution exceeds ``timeout_ms``
|
|
48
|
+
and ``MCPUnavailableError`` if the client cannot reach the MCP server
|
|
49
|
+
or kernel.
|
|
50
|
+
|
|
51
|
+
The timeout is non-blocking for the caller: detection uses
|
|
52
|
+
``concurrent.futures.wait`` (not ``future.result(timeout=...)`` inside a
|
|
53
|
+
``with ThreadPoolExecutor`` block, whose ``__exit__`` would otherwise
|
|
54
|
+
perform an implicit ``shutdown(wait=True)`` and block the caller until
|
|
55
|
+
the still-running worker finishes). The executor is shut down with
|
|
56
|
+
``wait=False`` so a worker that keeps running past the deadline cannot
|
|
57
|
+
delay the caller; its eventual result is discarded and never written to
|
|
58
|
+
the notebook, since only the value returned here is ever persisted.
|
|
59
|
+
"""
|
|
60
|
+
timeout_s = timeout_ms / 1000
|
|
61
|
+
executor = concurrent.futures.ThreadPoolExecutor(max_workers=1)
|
|
62
|
+
try:
|
|
63
|
+
future = executor.submit(client.execute, code)
|
|
64
|
+
try:
|
|
65
|
+
done, _pending = concurrent.futures.wait({future}, timeout=timeout_s)
|
|
66
|
+
if future not in done:
|
|
67
|
+
raise MCPExecutionTimeoutError(
|
|
68
|
+
f"Execution exceeded the configured {timeout_ms}ms timeout "
|
|
69
|
+
f"(実行が設定タイムアウト{timeout_ms}msを超えました)."
|
|
70
|
+
)
|
|
71
|
+
return future.result()
|
|
72
|
+
except ConnectionError as exc:
|
|
73
|
+
raise MCPUnavailableError(
|
|
74
|
+
"The configured Jupyter MCP server or kernel is unreachable "
|
|
75
|
+
"(設定されたJupyter MCPサーバー/カーネルに接続できません)."
|
|
76
|
+
) from exc
|
|
77
|
+
finally:
|
|
78
|
+
executor.shutdown(wait=False)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# @id CODE-AIDS-030
|
|
82
|
+
# @implements REQ-AIDS-030
|
|
83
|
+
# @design DES-AIDS-004
|
|
84
|
+
def run_and_record(
|
|
85
|
+
client: MCPClient,
|
|
86
|
+
handle: ProjectHandle,
|
|
87
|
+
code: str,
|
|
88
|
+
timeout_ms: int = DEFAULT_TIMEOUT_MS,
|
|
89
|
+
) -> dict:
|
|
90
|
+
"""Execute ``code`` and, only on success, append a cell to the notebook.
|
|
91
|
+
|
|
92
|
+
On failure the classified error propagates and no cell is written,
|
|
93
|
+
satisfying REQ-AIDS-030 (unavailability) and REQ-AIDS-031 (timeout).
|
|
94
|
+
"""
|
|
95
|
+
result = execute_cell(client, code, timeout_ms=timeout_ms)
|
|
96
|
+
stamped_count: dict[str, int] = {}
|
|
97
|
+
|
|
98
|
+
def add_cell(nb):
|
|
99
|
+
execution_count = next_execution_count(nb)
|
|
100
|
+
stamped_count["value"] = execution_count
|
|
101
|
+
cell = nbformat.v4.new_code_cell(code)
|
|
102
|
+
cell["execution_count"] = execution_count
|
|
103
|
+
cell["outputs"] = [
|
|
104
|
+
nbformat.v4.new_output(
|
|
105
|
+
"execute_result",
|
|
106
|
+
data={"text/plain": str(result.get("output", ""))},
|
|
107
|
+
execution_count=execution_count,
|
|
108
|
+
)
|
|
109
|
+
]
|
|
110
|
+
nb.cells.append(cell)
|
|
111
|
+
|
|
112
|
+
enqueue_write(handle, add_cell)
|
|
113
|
+
return {**result, "execution_count": stamped_count["value"]}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"""Jupyter MCP runtime lifecycle manager.
|
|
2
|
+
|
|
3
|
+
Implements DES-AIDS-025: on the first execution request with no healthy
|
|
4
|
+
runtime registered, installs (if needed) and starts JupyterLab and the
|
|
5
|
+
jupyter-mcp-server inside the project's managed Python environment, bound to
|
|
6
|
+
127.0.0.1 with an automatically selected free port and a randomly generated
|
|
7
|
+
token, persists that runtime's identity to a machine-local state file so a
|
|
8
|
+
separate later invocation reuses it instead of starting a duplicate one, and
|
|
9
|
+
exposes explicit status/stop operations.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import dataclasses
|
|
15
|
+
import json
|
|
16
|
+
import time
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Protocol
|
|
19
|
+
|
|
20
|
+
from ai_data_scientist.mcp_gateway import MCPUnavailableError
|
|
21
|
+
|
|
22
|
+
DEFAULT_STARTUP_TIMEOUT_MS = 30000
|
|
23
|
+
_HEALTH_POLL_INTERVAL_S = 0.2
|
|
24
|
+
DEFAULT_STATE_PATH = Path.home() / ".cache" / "ai-data-scientist" / "mcp_runtime.json"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclasses.dataclass(frozen=True)
|
|
28
|
+
class RuntimeInfo:
|
|
29
|
+
"""Identity of a running Jupyter MCP runtime (REQ-AIDS-034).
|
|
30
|
+
|
|
31
|
+
Two separate (port, token) pairs are tracked because JupyterLab and the
|
|
32
|
+
jupyter-mcp-server (streamable-http transport) are independent processes
|
|
33
|
+
with independent listeners and independent authentication: JUPYTER_TOKEN
|
|
34
|
+
guards the Jupyter server itself, while the MCP token guards the MCP
|
|
35
|
+
server's own HTTP endpoint that the concrete client (DES-AIDS-026) calls.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
jupyter_pid: int
|
|
39
|
+
jupyter_port: int
|
|
40
|
+
jupyter_token: str
|
|
41
|
+
mcp_server_pid: int
|
|
42
|
+
mcp_port: int
|
|
43
|
+
mcp_token: str
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# @id CODE-AIDS-050
|
|
47
|
+
# @implements REQ-AIDS-041
|
|
48
|
+
# @design DES-AIDS-029
|
|
49
|
+
@dataclasses.dataclass(frozen=True)
|
|
50
|
+
class StopResult:
|
|
51
|
+
"""Outcome of a ``stop(..., wait=True)`` call (REQ-AIDS-041).
|
|
52
|
+
|
|
53
|
+
``still_running`` lists any recorded PIDs that were still alive when the
|
|
54
|
+
bounded poll timed out; empty when every recorded process exited before
|
|
55
|
+
the timeout.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
still_running: tuple[int, ...]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class RuntimeLauncher(Protocol):
|
|
62
|
+
"""Process-management contract any Jupyter MCP runtime backend must satisfy.
|
|
63
|
+
|
|
64
|
+
Kept as an injectable Protocol (mirroring mcp_gateway.MCPClient) so the
|
|
65
|
+
lifecycle logic below is fully unit-testable without actually installing
|
|
66
|
+
or starting JupyterLab/jupyter-mcp-server.
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
def start_jupyter(self) -> tuple[int, int, str]: ...
|
|
70
|
+
def start_mcp_server(self, jupyter_port: int, jupyter_token: str) -> tuple[int, int, str]: ...
|
|
71
|
+
def is_healthy(self, info: RuntimeInfo) -> bool: ...
|
|
72
|
+
def terminate(self, pid: int) -> None: ...
|
|
73
|
+
def is_process_alive(self, pid: int) -> bool: ...
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _read_state(state_path: Path) -> RuntimeInfo | None:
|
|
77
|
+
if not state_path.exists():
|
|
78
|
+
return None
|
|
79
|
+
try:
|
|
80
|
+
data = json.loads(state_path.read_text(encoding="utf-8"))
|
|
81
|
+
return RuntimeInfo(**data)
|
|
82
|
+
except (json.JSONDecodeError, TypeError, KeyError):
|
|
83
|
+
return None
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _write_state(state_path: Path, info: RuntimeInfo) -> None:
|
|
87
|
+
state_path.parent.mkdir(parents=True, exist_ok=True)
|
|
88
|
+
state_path.write_text(json.dumps(dataclasses.asdict(info)), encoding="utf-8")
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _clear_state(state_path: Path) -> None:
|
|
92
|
+
state_path.unlink(missing_ok=True)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
# @id CODE-AIDS-039
|
|
96
|
+
# @implements REQ-AIDS-034, REQ-AIDS-035, REQ-AIDS-036
|
|
97
|
+
# @design DES-AIDS-025
|
|
98
|
+
def ensure_runtime(
|
|
99
|
+
launcher: RuntimeLauncher,
|
|
100
|
+
state_path: Path = DEFAULT_STATE_PATH,
|
|
101
|
+
timeout_ms: int = DEFAULT_STARTUP_TIMEOUT_MS,
|
|
102
|
+
) -> RuntimeInfo:
|
|
103
|
+
"""Return a healthy Jupyter MCP runtime, starting one only if needed.
|
|
104
|
+
|
|
105
|
+
Reuses a still-running runtime recorded in ``state_path`` (REQ-AIDS-035).
|
|
106
|
+
Otherwise starts a new localhost-only runtime with an auto-selected port
|
|
107
|
+
and a random token (REQ-AIDS-034). Raises ``MCPUnavailableError`` without
|
|
108
|
+
registering a partially started runtime if it does not become healthy
|
|
109
|
+
within ``timeout_ms`` (REQ-AIDS-036).
|
|
110
|
+
"""
|
|
111
|
+
existing = _read_state(state_path)
|
|
112
|
+
if existing is not None and launcher.is_healthy(existing):
|
|
113
|
+
return existing
|
|
114
|
+
if existing is not None:
|
|
115
|
+
_clear_state(state_path)
|
|
116
|
+
|
|
117
|
+
jupyter_pid, jupyter_port, jupyter_token = launcher.start_jupyter()
|
|
118
|
+
mcp_server_pid, mcp_port, mcp_token = launcher.start_mcp_server(jupyter_port, jupyter_token)
|
|
119
|
+
candidate = RuntimeInfo(
|
|
120
|
+
jupyter_pid=jupyter_pid,
|
|
121
|
+
jupyter_port=jupyter_port,
|
|
122
|
+
jupyter_token=jupyter_token,
|
|
123
|
+
mcp_server_pid=mcp_server_pid,
|
|
124
|
+
mcp_port=mcp_port,
|
|
125
|
+
mcp_token=mcp_token,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
deadline = time.monotonic() + timeout_ms / 1000
|
|
129
|
+
while time.monotonic() < deadline:
|
|
130
|
+
if launcher.is_healthy(candidate):
|
|
131
|
+
_write_state(state_path, candidate)
|
|
132
|
+
return candidate
|
|
133
|
+
time.sleep(_HEALTH_POLL_INTERVAL_S)
|
|
134
|
+
|
|
135
|
+
launcher.terminate(jupyter_pid)
|
|
136
|
+
launcher.terminate(mcp_server_pid)
|
|
137
|
+
raise MCPUnavailableError(
|
|
138
|
+
"Jupyter MCP runtime did not become healthy within "
|
|
139
|
+
f"{timeout_ms}ms (Jupyter MCPランタイムが{timeout_ms}ms以内に起動しませんでした)."
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
# @id CODE-AIDS-040
|
|
144
|
+
# @implements REQ-AIDS-037
|
|
145
|
+
# @design DES-AIDS-025
|
|
146
|
+
def status(launcher: RuntimeLauncher, state_path: Path = DEFAULT_STATE_PATH) -> RuntimeInfo | None:
|
|
147
|
+
"""Return the currently running, healthy runtime, or None if none is running."""
|
|
148
|
+
existing = _read_state(state_path)
|
|
149
|
+
if existing is None:
|
|
150
|
+
return None
|
|
151
|
+
if launcher.is_healthy(existing):
|
|
152
|
+
return existing
|
|
153
|
+
_clear_state(state_path)
|
|
154
|
+
return None
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
# @id CODE-AIDS-041
|
|
158
|
+
# @implements REQ-AIDS-037
|
|
159
|
+
# @design DES-AIDS-025
|
|
160
|
+
def stop(
|
|
161
|
+
launcher: RuntimeLauncher,
|
|
162
|
+
state_path: Path = DEFAULT_STATE_PATH,
|
|
163
|
+
wait: bool = False,
|
|
164
|
+
timeout_s: float = 5.0,
|
|
165
|
+
poll_interval_s: float = 0.2,
|
|
166
|
+
) -> StopResult | None:
|
|
167
|
+
"""Terminate the recorded runtime's processes and clear the state file.
|
|
168
|
+
|
|
169
|
+
With the default ``wait=False`` this is fire-and-forget (unchanged
|
|
170
|
+
behavior): it sends SIGTERM and returns ``None`` immediately without
|
|
171
|
+
confirming the processes actually exited. With ``wait=True``
|
|
172
|
+
(REQ-AIDS-041), it polls ``launcher.is_process_alive`` for both recorded
|
|
173
|
+
PIDs every ``poll_interval_s`` seconds until neither is alive or
|
|
174
|
+
``timeout_s`` elapses, then returns a ``StopResult`` recording any PIDs
|
|
175
|
+
still alive at that point (empty when both exited cleanly).
|
|
176
|
+
"""
|
|
177
|
+
existing = _read_state(state_path)
|
|
178
|
+
if existing is None:
|
|
179
|
+
return StopResult(still_running=()) if wait else None
|
|
180
|
+
launcher.terminate(existing.jupyter_pid)
|
|
181
|
+
launcher.terminate(existing.mcp_server_pid)
|
|
182
|
+
_clear_state(state_path)
|
|
183
|
+
|
|
184
|
+
if not wait:
|
|
185
|
+
return None
|
|
186
|
+
|
|
187
|
+
pids = [existing.jupyter_pid, existing.mcp_server_pid]
|
|
188
|
+
deadline = time.monotonic() + timeout_s
|
|
189
|
+
while time.monotonic() < deadline:
|
|
190
|
+
pids = [pid for pid in pids if launcher.is_process_alive(pid)]
|
|
191
|
+
if not pids:
|
|
192
|
+
break
|
|
193
|
+
time.sleep(poll_interval_s)
|
|
194
|
+
return StopResult(still_running=tuple(pids))
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Real streamable-http transport for the Jupyter MCP server.
|
|
2
|
+
|
|
3
|
+
Implements the ``_Transport`` callable jupyter_mcp_client.JupyterMCPClient
|
|
4
|
+
expects, using the real ``mcp`` Python SDK. Verified interactively against a
|
|
5
|
+
live jupyter-mcp-server 2.2.3: the installed SDK exposes
|
|
6
|
+
``mcp.client.streamable_http.streamable_http_client`` (not
|
|
7
|
+
``streamablehttp_client``), yields a 2-tuple ``(read, write)``, accepts auth
|
|
8
|
+
headers only via an injected ``httpx.AsyncClient``, and each discovered
|
|
9
|
+
``Tool`` exposes its JSON schema as ``input_schema`` (snake_case).
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import asyncio
|
|
15
|
+
|
|
16
|
+
import httpx
|
|
17
|
+
from mcp import ClientSession
|
|
18
|
+
from mcp.client.streamable_http import streamable_http_client
|
|
19
|
+
|
|
20
|
+
_TOOL_NAME = "execute_code"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
async def _execute_code_async(port: int, token: str, code: str) -> dict:
|
|
24
|
+
url = f"http://127.0.0.1:{port}/mcp"
|
|
25
|
+
headers = {"Authorization": "Bearer " + token}
|
|
26
|
+
async with (
|
|
27
|
+
httpx.AsyncClient(headers=headers) as http_client,
|
|
28
|
+
streamable_http_client(url, http_client=http_client) as (read, write),
|
|
29
|
+
ClientSession(read, write) as session,
|
|
30
|
+
):
|
|
31
|
+
await session.initialize()
|
|
32
|
+
result = await session.call_tool(_TOOL_NAME, {"code": code})
|
|
33
|
+
if result.is_error:
|
|
34
|
+
text = "".join(block.text for block in result.content if hasattr(block, "text"))
|
|
35
|
+
return {"status": "error", "output": text}
|
|
36
|
+
outputs = (result.structured_content or {}).get("outputs")
|
|
37
|
+
if outputs is not None:
|
|
38
|
+
output = "\n".join(str(item) for item in outputs)
|
|
39
|
+
else:
|
|
40
|
+
output = "".join(block.text for block in result.content if hasattr(block, "text"))
|
|
41
|
+
return {"status": "ok", "output": output}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# @id CODE-AIDS-045
|
|
45
|
+
# @implements REQ-AIDS-038
|
|
46
|
+
# @design DES-AIDS-026
|
|
47
|
+
def execute_code(port: int, token: str, code: str) -> dict:
|
|
48
|
+
"""Run ``code`` on a live jupyter-mcp-server and return its result.
|
|
49
|
+
|
|
50
|
+
Matches the ``_Transport`` Protocol expected by JupyterMCPClient:
|
|
51
|
+
``(port, token, code) -> {"status": "ok"|"error", "output": str}``.
|
|
52
|
+
"""
|
|
53
|
+
return asyncio.run(_execute_code_async(port, token, code))
|