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.
Files changed (193) hide show
  1. package/.github/skills/ai-chemistry-scientist/SKILL.md +97 -0
  2. package/.github/skills/ai-chemistry-scientist/manifest.json +156 -0
  3. package/.github/skills/ai-data-scientist/SKILL.md +330 -0
  4. package/.github/skills/ai-genomics-scientist/SKILL.md +98 -0
  5. package/.github/skills/ai-genomics-scientist/manifest.json +93 -0
  6. package/.github/skills/ai-materials-scientist/SKILL.md +51 -0
  7. package/.github/skills/ai-materials-scientist/manifest.json +58 -0
  8. package/.github/skills/ai-scientist/SKILL.md +69 -0
  9. package/.github/skills/ai-scientist/manifest.json +61 -0
  10. package/.github/skills/ai-structural-biology-scientist/SKILL.md +67 -0
  11. package/.github/skills/ai-structural-biology-scientist/manifest.json +72 -0
  12. package/.github/skills/japanese-prose/NOTICE.md +17 -0
  13. package/.github/skills/japanese-prose/SKILL.md +111 -0
  14. package/.github/skills/japanese-prose/references/review-workflow.md +50 -0
  15. package/.github/skills/japanese-prose/references/scoring.md +24 -0
  16. package/.github/skills/japanese-prose/references/writing-guidelines.md +60 -0
  17. package/.github/skills/japanese-prose/scripts/core.py +192 -0
  18. package/.github/skills/japanese-prose/scripts/fixtures/natural.md +5 -0
  19. package/.github/skills/japanese-prose/scripts/fixtures/unnatural.md +5 -0
  20. package/.github/skills/japanese-prose/scripts/lint.py +378 -0
  21. package/.github/skills/japanese-prose/scripts/outline.py +68 -0
  22. package/.github/skills/japanese-prose/scripts/terms.py +112 -0
  23. package/.github/skills/japanese-prose/scripts/test_engine.py +117 -0
  24. package/.github/skills/presentation-planner/SKILL.md +257 -0
  25. package/.github/skills/presentation-planner/assets/design-templates/data-report.yaml +97 -0
  26. package/.github/skills/presentation-planner/assets/design-templates/executive-proposal.yaml +92 -0
  27. package/.github/skills/presentation-planner/assets/design-templates/technical-briefing.yaml +96 -0
  28. package/.github/skills/presentation-planner/assets/scenario-templates/data-report.md +47 -0
  29. package/.github/skills/presentation-planner/assets/scenario-templates/executive-decision.md +43 -0
  30. package/.github/skills/presentation-planner/assets/scenario-templates/technical-briefing.md +45 -0
  31. package/.github/skills/presentation-planner/references/customizing-design-templates.md +160 -0
  32. package/.github/skills/presentation-planner/references/design-spec-schema.md +72 -0
  33. package/.github/skills/presentation-planner/references/handoff-contract.md +49 -0
  34. package/.github/skills/presentation-planner/references/responsibility-boundary.md +32 -0
  35. package/.github/skills/presentation-planner/references/scenario-templates.md +55 -0
  36. package/.github/skills/tech-writer/SKILL.md +434 -0
  37. package/.github/skills/tech-writer/assets/templates/blueprint.md +187 -0
  38. package/.github/skills/tech-writer/assets/templates/design-doc.md +29 -0
  39. package/.github/skills/tech-writer/assets/templates/migration-plan.md +173 -0
  40. package/.github/skills/tech-writer/assets/templates/operations-runbook.md +202 -0
  41. package/.github/skills/tech-writer/assets/templates/pr-description.md +23 -0
  42. package/.github/skills/tech-writer/assets/templates/qiita.md +44 -0
  43. package/.github/skills/tech-writer/assets/templates/readme.md +38 -0
  44. package/.github/skills/tech-writer/assets/templates/requirements-definition.md +170 -0
  45. package/.github/skills/tech-writer/assets/templates/rfi.md +113 -0
  46. package/.github/skills/tech-writer/assets/templates/rfp.md +180 -0
  47. package/.github/skills/tech-writer/assets/templates/security-design.md +167 -0
  48. package/.github/skills/tech-writer/assets/templates/system-design.md +220 -0
  49. package/.github/skills/tech-writer/assets/templates/technical-proposal.md +112 -0
  50. package/.github/skills/tech-writer/assets/templates/test-plan.md +153 -0
  51. package/.github/skills/tech-writer/assets/templates/user-manual.md +22 -0
  52. package/.github/skills/tech-writer/assets/templates/white-paper.md +192 -0
  53. package/.github/skills/tech-writer/references/doctypes/api-docs.md +33 -0
  54. package/.github/skills/tech-writer/references/doctypes/blueprint.md +81 -0
  55. package/.github/skills/tech-writer/references/doctypes/code-comments.md +39 -0
  56. package/.github/skills/tech-writer/references/doctypes/design-doc.md +42 -0
  57. package/.github/skills/tech-writer/references/doctypes/migration-plan.md +63 -0
  58. package/.github/skills/tech-writer/references/doctypes/operations-runbook.md +63 -0
  59. package/.github/skills/tech-writer/references/doctypes/pr-commit.md +82 -0
  60. package/.github/skills/tech-writer/references/doctypes/qiita.md +75 -0
  61. package/.github/skills/tech-writer/references/doctypes/readme.md +43 -0
  62. package/.github/skills/tech-writer/references/doctypes/release-notes.md +30 -0
  63. package/.github/skills/tech-writer/references/doctypes/requirements-definition.md +61 -0
  64. package/.github/skills/tech-writer/references/doctypes/rfi.md +43 -0
  65. package/.github/skills/tech-writer/references/doctypes/rfp.md +46 -0
  66. package/.github/skills/tech-writer/references/doctypes/security-design.md +71 -0
  67. package/.github/skills/tech-writer/references/doctypes/system-design.md +74 -0
  68. package/.github/skills/tech-writer/references/doctypes/technical-proposal.md +49 -0
  69. package/.github/skills/tech-writer/references/doctypes/test-plan.md +67 -0
  70. package/.github/skills/tech-writer/references/doctypes/user-manual.md +58 -0
  71. package/.github/skills/tech-writer/references/doctypes/white-paper.md +84 -0
  72. package/.github/skills/tech-writer/references/doctypes/zenn.md +66 -0
  73. package/.github/skills/tech-writer/references/japanese-prose-optimization.md +110 -0
  74. package/.github/skills/tech-writer/references/style-constitution.md +104 -0
  75. package/.github/skills/tech-writer/scripts/lint.py +412 -0
  76. package/LICENSE +21 -0
  77. package/README.md +92 -0
  78. package/bin/ai-data-scientist.js +123 -0
  79. package/package.json +41 -0
  80. package/pyproject.toml +45 -0
  81. package/src/ai_chemistry_scientist/__init__.py +0 -0
  82. package/src/ai_chemistry_scientist/admet_prediction.py +71 -0
  83. package/src/ai_chemistry_scientist/bioactivity_classification.py +73 -0
  84. package/src/ai_chemistry_scientist/data/sample_molecules.csv +21 -0
  85. package/src/ai_chemistry_scientist/dispatch.py +369 -0
  86. package/src/ai_chemistry_scientist/docking_score.py +97 -0
  87. package/src/ai_chemistry_scientist/drug_likeness_rules.py +84 -0
  88. package/src/ai_chemistry_scientist/evidence.py +41 -0
  89. package/src/ai_chemistry_scientist/molecular_descriptors.py +97 -0
  90. package/src/ai_chemistry_scientist/molecular_formula_mass.py +40 -0
  91. package/src/ai_chemistry_scientist/molecular_similarity.py +78 -0
  92. package/src/ai_chemistry_scientist/qsar_modeling.py +105 -0
  93. package/src/ai_chemistry_scientist/salt_standardization.py +81 -0
  94. package/src/ai_chemistry_scientist/structural_alerts.py +76 -0
  95. package/src/ai_chemistry_scientist/structure_format_conversion.py +84 -0
  96. package/src/ai_chemistry_scientist/validation.py +70 -0
  97. package/src/ai_data_scientist/__init__.py +0 -0
  98. package/src/ai_data_scientist/analysis_assumptions.py +121 -0
  99. package/src/ai_data_scientist/anomaly_detection.py +39 -0
  100. package/src/ai_data_scientist/automl.py +109 -0
  101. package/src/ai_data_scientist/cleaning.py +56 -0
  102. package/src/ai_data_scientist/cli.py +90 -0
  103. package/src/ai_data_scientist/clustering.py +54 -0
  104. package/src/ai_data_scientist/dashboard.py +33 -0
  105. package/src/ai_data_scientist/data_definition.py +100 -0
  106. package/src/ai_data_scientist/data_quality.py +164 -0
  107. package/src/ai_data_scientist/dataset_validation.py +135 -0
  108. package/src/ai_data_scientist/dependency_pins.py +60 -0
  109. package/src/ai_data_scientist/eda.py +82 -0
  110. package/src/ai_data_scientist/experiment_evaluation.py +635 -0
  111. package/src/ai_data_scientist/explainability.py +340 -0
  112. package/src/ai_data_scientist/feature_engineering.py +163 -0
  113. package/src/ai_data_scientist/gate_config.py +32 -0
  114. package/src/ai_data_scientist/ingestion.py +127 -0
  115. package/src/ai_data_scientist/insight_engine.py +180 -0
  116. package/src/ai_data_scientist/japanese_nlp.py +43 -0
  117. package/src/ai_data_scientist/jupyter_launcher.py +137 -0
  118. package/src/ai_data_scientist/jupyter_mcp_client.py +94 -0
  119. package/src/ai_data_scientist/language_router.py +28 -0
  120. package/src/ai_data_scientist/lifecycle.py +221 -0
  121. package/src/ai_data_scientist/mcp_gateway.py +113 -0
  122. package/src/ai_data_scientist/mcp_runtime.py +194 -0
  123. package/src/ai_data_scientist/mcp_transport.py +53 -0
  124. package/src/ai_data_scientist/ml_modeling.py +451 -0
  125. package/src/ai_data_scientist/model_tuning.py +104 -0
  126. package/src/ai_data_scientist/notebook_audit.py +574 -0
  127. package/src/ai_data_scientist/project_manager.py +243 -0
  128. package/src/ai_data_scientist/report_export.py +73 -0
  129. package/src/ai_data_scientist/sensitivity.py +445 -0
  130. package/src/ai_data_scientist/signal_analysis.py +201 -0
  131. package/src/ai_data_scientist/skill_packaging.py +40 -0
  132. package/src/ai_data_scientist/stats_analysis.py +88 -0
  133. package/src/ai_data_scientist/text_nlp.py +44 -0
  134. package/src/ai_data_scientist/timeseries.py +68 -0
  135. package/src/ai_data_scientist/visualization.py +708 -0
  136. package/src/ai_genomics_scientist/__init__.py +1 -0
  137. package/src/ai_genomics_scientist/differential_expression.py +147 -0
  138. package/src/ai_genomics_scientist/dispatch.py +267 -0
  139. package/src/ai_genomics_scientist/evidence.py +45 -0
  140. package/src/ai_genomics_scientist/gene_set_enrichment.py +76 -0
  141. package/src/ai_genomics_scientist/sequence_alignment.py +97 -0
  142. package/src/ai_genomics_scientist/sequence_features.py +111 -0
  143. package/src/ai_genomics_scientist/splice_site_scoring.py +66 -0
  144. package/src/ai_genomics_scientist/validation.py +83 -0
  145. package/src/ai_genomics_scientist/variant_effect.py +147 -0
  146. package/src/ai_genomics_scientist/variant_pathogenicity.py +125 -0
  147. package/src/ai_materials_scientist/__init__.py +0 -0
  148. package/src/ai_materials_scientist/calphad.py +117 -0
  149. package/src/ai_materials_scientist/classical_monte_carlo.py +165 -0
  150. package/src/ai_materials_scientist/crystal_plasticity.py +184 -0
  151. package/src/ai_materials_scientist/dispatch.py +100 -0
  152. package/src/ai_materials_scientist/evidence.py +84 -0
  153. package/src/ai_materials_scientist/fem.py +279 -0
  154. package/src/ai_materials_scientist/kinetic_monte_carlo.py +145 -0
  155. package/src/ai_materials_scientist/molecular_dynamics.py +240 -0
  156. package/src/ai_materials_scientist/phase_field.py +167 -0
  157. package/src/ai_materials_scientist/validation.py +70 -0
  158. package/src/ai_scientist/__init__.py +1 -0
  159. package/src/ai_scientist/completion_gate.py +15 -0
  160. package/src/ai_scientist/data_analysis.py +46 -0
  161. package/src/ai_scientist/evidence_registry.py +99 -0
  162. package/src/ai_scientist/experimental_design.py +20 -0
  163. package/src/ai_scientist/language.py +14 -0
  164. package/src/ai_scientist/latex_renderer.py +41 -0
  165. package/src/ai_scientist/literature_review.py +37 -0
  166. package/src/ai_scientist/manifest.py +87 -0
  167. package/src/ai_scientist/manuscript.py +94 -0
  168. package/src/ai_scientist/mcp_config.py +76 -0
  169. package/src/ai_scientist/mcp_external.py +42 -0
  170. package/src/ai_scientist/mcp_failures.py +23 -0
  171. package/src/ai_scientist/mcp_gateway.py +38 -0
  172. package/src/ai_scientist/mcp_managed.py +180 -0
  173. package/src/ai_scientist/npm_packaging.py +49 -0
  174. package/src/ai_scientist/orchestrator.py +133 -0
  175. package/src/ai_scientist/peer_review.py +60 -0
  176. package/src/ai_scientist/phase_gate.py +74 -0
  177. package/src/ai_scientist/phase_state.py +230 -0
  178. package/src/ai_scientist/presentation.py +56 -0
  179. package/src/ai_scientist/project_config.py +31 -0
  180. package/src/ai_scientist/project_handle.py +74 -0
  181. package/src/ai_scientist/reproducibility.py +20 -0
  182. package/src/ai_scientist/research_planning.py +20 -0
  183. package/src/ai_scientist/skill_invocation.py +21 -0
  184. package/src/ai_scientist/tdd_gate.py +99 -0
  185. package/src/ai_structural_biology_scientist/__init__.py +0 -0
  186. package/src/ai_structural_biology_scientist/contact_map.py +87 -0
  187. package/src/ai_structural_biology_scientist/dispatch.py +269 -0
  188. package/src/ai_structural_biology_scientist/evidence.py +43 -0
  189. package/src/ai_structural_biology_scientist/hydrophobicity.py +101 -0
  190. package/src/ai_structural_biology_scientist/protein_docking_score.py +104 -0
  191. package/src/ai_structural_biology_scientist/secondary_structure.py +95 -0
  192. package/src/ai_structural_biology_scientist/structural_similarity.py +74 -0
  193. 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))