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,94 @@
1
+ """Manuscript-writing delegation and final rendering."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from datetime import datetime, timezone
6
+ from pathlib import Path
7
+
8
+ from ai_scientist.evidence_registry import query_evidence, record_evidence
9
+ from ai_scientist.language import detect_language
10
+ from ai_scientist.latex_renderer import render_latex
11
+ from ai_scientist.project_config import load_project_config
12
+ from ai_scientist.project_handle import ResearchProjectHandle
13
+ from ai_scientist.skill_invocation import SkillInvoker
14
+
15
+ TECH_WRITER_DEPENDENCY = {"skillId": "tech-writer", "version": "0.3.0"}
16
+
17
+
18
+ def _now() -> str:
19
+ return datetime.now(timezone.utc).isoformat()
20
+
21
+
22
+ def _research_evidence_manifest(handle: ResearchProjectHandle) -> list[dict]:
23
+ return [
24
+ {
25
+ "project": record.project,
26
+ "phase": record.phase,
27
+ "artifactPath": record.artifact_path,
28
+ "artifactKind": record.artifact_kind,
29
+ "createdAt": record.created_at,
30
+ "metadata": record.metadata,
31
+ }
32
+ for record in query_evidence(handle)
33
+ if record.phase
34
+ in {"research-planning", "literature-review", "experimental-design", "data-analysis"}
35
+ ]
36
+
37
+
38
+ def _write_markdown_artifact(handle: ResearchProjectHandle, content: str) -> Path:
39
+ path = handle.manuscript_dir / "manuscript.md"
40
+ path.write_text(content, encoding="utf-8")
41
+ return path
42
+
43
+
44
+ # @id CODE-AISCI-010
45
+ # @implements REQ-AISCI-009
46
+ # @design DES-AISCI-006
47
+ # @id CODE-AISCI-011
48
+ # @implements REQ-AISCI-010
49
+ # @design DES-AISCI-007
50
+ # @id CODE-AISCI-022
51
+ # @implements REQ-AISCI-021, REQ-AISCI-022
52
+ # @design DES-AISCI-018
53
+ def write_manuscript(
54
+ handle: ResearchProjectHandle, instruction: str, invoker: SkillInvoker
55
+ ) -> dict:
56
+ """Delegate manuscript drafting to tech-writer and render final format."""
57
+ evidence_manifest = _research_evidence_manifest(handle)
58
+ payload = {
59
+ "project": {"name": handle.name, "root": str(handle.root)},
60
+ "instruction": instruction,
61
+ "evidenceManifest": evidence_manifest,
62
+ }
63
+ response = invoker.invoke(
64
+ TECH_WRITER_DEPENDENCY["skillId"],
65
+ TECH_WRITER_DEPENDENCY["version"],
66
+ "write",
67
+ payload,
68
+ )
69
+ markdown_path = _write_markdown_artifact(handle, response["content"])
70
+ language = detect_language(instruction)
71
+ config = load_project_config(handle)
72
+ manuscript_format = config.get("manuscriptFormat", "markdown")
73
+ final_path = markdown_path
74
+ artifact_kind = "markdown"
75
+ if manuscript_format == "latex":
76
+ final_path = handle.manuscript_dir / "manuscript.tex"
77
+ final_path.write_text(render_latex(markdown_path), encoding="utf-8")
78
+ artifact_kind = "latex"
79
+ record = record_evidence(
80
+ handle,
81
+ "manuscript-writing",
82
+ final_path,
83
+ artifact_kind,
84
+ _now(),
85
+ metadata={"language": language, "sourceMarkdownPath": str(markdown_path)},
86
+ )
87
+ return {
88
+ "phase": "manuscript-writing",
89
+ "artifact": {
90
+ "phase": record.phase,
91
+ "path": record.artifact_path,
92
+ "metadata": record.metadata,
93
+ },
94
+ }
@@ -0,0 +1,76 @@
1
+ """MCP server configuration loading for ai_scientist."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from dataclasses import dataclass
7
+ from pathlib import Path
8
+
9
+
10
+ class McpConfigError(ValueError):
11
+ """Invalid MCP configuration."""
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class ManagedServerConfig:
16
+ name: str
17
+ mode: str
18
+ launch_command: str
19
+ endpoint_template: str
20
+ health_check_path: str
21
+
22
+
23
+ @dataclass(frozen=True)
24
+ class ExternalServerConfig:
25
+ name: str
26
+ mode: str
27
+ endpoint: str
28
+
29
+
30
+ def _require(payload: dict, field: str, server_name: str) -> str:
31
+ value = payload.get(field)
32
+ if not value:
33
+ raise McpConfigError(f"{server_name}: missing required field '{field}'")
34
+ return value
35
+
36
+
37
+ # @id CODE-AISCI-016
38
+ # @implements REQ-AISCI-016
39
+ # @design DES-AISCI-011
40
+ def load_mcp_config(
41
+ config_path: Path | str,
42
+ ) -> dict[str, ManagedServerConfig | ExternalServerConfig]:
43
+ """Load and validate MCP server entries."""
44
+ payload = json.loads(Path(config_path).read_text(encoding="utf-8"))
45
+ servers = payload.get("servers", [])
46
+ validated: dict[str, ManagedServerConfig | ExternalServerConfig] = {}
47
+ for entry in servers:
48
+ name = _require(entry, "name", "<unnamed>")
49
+ if name in validated:
50
+ raise McpConfigError(f"{name}: duplicate server name")
51
+ mode = _require(entry, "mode", name)
52
+ if mode == "managed":
53
+ launch_command = _require(entry, "launchCommand", name)
54
+ endpoint_template = _require(entry, "endpointTemplate", name)
55
+ health_check_path = _require(entry, "healthCheckPath", name)
56
+ if "{port}" not in launch_command:
57
+ raise McpConfigError(f"{name}: launchCommand must contain '{{port}}'")
58
+ if "{port}" not in endpoint_template:
59
+ raise McpConfigError(f"{name}: endpointTemplate must contain '{{port}}'")
60
+ validated[name] = ManagedServerConfig(
61
+ name=name,
62
+ mode=mode,
63
+ launch_command=launch_command,
64
+ endpoint_template=endpoint_template,
65
+ health_check_path=health_check_path,
66
+ )
67
+ continue
68
+ if mode == "external":
69
+ validated[name] = ExternalServerConfig(
70
+ name=name,
71
+ mode=mode,
72
+ endpoint=_require(entry, "endpoint", name),
73
+ )
74
+ continue
75
+ raise McpConfigError(f"{name}: mode must be 'managed' or 'external'")
76
+ return validated
@@ -0,0 +1,42 @@
1
+ """External MCP endpoint connector."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from dataclasses import dataclass
7
+ from urllib import error, request
8
+
9
+ from ai_scientist.mcp_config import ExternalServerConfig
10
+
11
+
12
+ @dataclass(frozen=True)
13
+ class HttpMcpClient:
14
+ """Simple HTTP MCP client used by the tests and local runtime glue."""
15
+
16
+ endpoint: str
17
+
18
+ def call_tool(self, tool_name: str, args: dict) -> dict:
19
+ payload = json.dumps({"tool": tool_name, "args": args}).encode("utf-8")
20
+ req = request.Request(
21
+ url=f"{self.endpoint.rstrip('/')}/tool",
22
+ data=payload,
23
+ headers={"Content-Type": "application/json"},
24
+ method="POST",
25
+ )
26
+ try:
27
+ with request.urlopen(req, timeout=3) as response:
28
+ return json.loads(response.read().decode("utf-8"))
29
+ except (error.URLError, ConnectionError, TimeoutError) as exc:
30
+ raise ConnectionError(str(exc)) from exc
31
+
32
+
33
+ # @id CODE-AISCI-018
34
+ # @implements REQ-AISCI-018
35
+ # @design DES-AISCI-013
36
+ def connect_external_server(entry: ExternalServerConfig) -> HttpMcpClient:
37
+ """Return a client without managing any process lifecycle."""
38
+ if not isinstance(entry, ExternalServerConfig):
39
+ raise ValueError(
40
+ f"connect_external_server requires an external-mode config entry, got {entry!r}"
41
+ )
42
+ return HttpMcpClient(entry.endpoint)
@@ -0,0 +1,23 @@
1
+ """Failure classification for ai_scientist MCP access."""
2
+
3
+ from __future__ import annotations
4
+
5
+
6
+ class MCPUnavailableError(RuntimeError):
7
+ """MCP server unavailability with explicit server/phase attribution."""
8
+
9
+ def __init__(self, server_name: str, phase: str, error: Exception | str):
10
+ self.server_name = server_name
11
+ self.phase = phase
12
+ detail = str(error)
13
+ super().__init__(f"MCP server '{server_name}' is unreachable for phase '{phase}': {detail}")
14
+
15
+
16
+ # @id CODE-AISCI-019
17
+ # @implements REQ-AISCI-019
18
+ # @design DES-AISCI-014
19
+ def classify_mcp_failure(
20
+ server_name: str, phase: str, error: Exception | str
21
+ ) -> MCPUnavailableError:
22
+ """Build the classified error the caller must report."""
23
+ return MCPUnavailableError(server_name, phase, error)
@@ -0,0 +1,38 @@
1
+ """MCP gateway that attributes calls to phases."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Protocol
7
+
8
+ from ai_scientist.mcp_failures import MCPUnavailableError, classify_mcp_failure
9
+
10
+
11
+ class McpClient(Protocol):
12
+ """Minimal domain-tool client contract."""
13
+
14
+ def call_tool(self, tool_name: str, args: dict) -> dict: ...
15
+
16
+
17
+ @dataclass
18
+ class McpGateway:
19
+ """Route all ai_scientist external tool access through configured clients."""
20
+
21
+ clients: dict[str, McpClient]
22
+
23
+ # @id CODE-AISCI-015
24
+ # @implements REQ-AISCI-015
25
+ # @design DES-AISCI-010
26
+ def call_tool(self, server_name: str, tool_name: str, args: dict, phase: str) -> dict:
27
+ """Call one MCP tool via the configured client or raise a classified error."""
28
+ try:
29
+ client = self.clients[server_name]
30
+ except KeyError as exc:
31
+ raise classify_mcp_failure(server_name, phase, exc) from exc
32
+ try:
33
+ return client.call_tool(tool_name, args)
34
+ except Exception as exc: # noqa: BLE001 - classified into explicit domain error
35
+ raise classify_mcp_failure(server_name, phase, exc) from exc
36
+
37
+
38
+ __all__ = ["McpGateway", "MCPUnavailableError"]
@@ -0,0 +1,180 @@
1
+ """Managed MCP process lifecycle for ai_scientist."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import ipaddress
6
+ import shlex
7
+ import socket
8
+ import subprocess
9
+ import threading
10
+ import time
11
+ from dataclasses import dataclass
12
+ from urllib import error, request
13
+ from urllib.parse import urlparse
14
+
15
+ from ai_scientist.mcp_config import ManagedServerConfig
16
+ from ai_scientist.mcp_failures import classify_mcp_failure
17
+
18
+
19
+ @dataclass(frozen=True)
20
+ class RuntimeInfo:
21
+ """Runtime identity for one managed MCP server."""
22
+
23
+ pid: int
24
+ port: int
25
+ endpoint: str
26
+
27
+
28
+ _RUNTIMES: dict[str, tuple[subprocess.Popen, RuntimeInfo, str]] = {}
29
+
30
+ # Per-server-name locks, guarded by a single registry lock so lock creation
31
+ # itself cannot race. Mirrors the ai_data_scientist project_manager per-path
32
+ # lock pattern, but here the lock additionally protects the entire
33
+ # check-then-start critical section in ensure_managed_server, so two
34
+ # concurrent first-use callers for the same server name cannot both observe
35
+ # "no running process" and both start a second process.
36
+ _runtime_locks: dict[str, threading.Lock] = {}
37
+ _runtime_locks_guard = threading.Lock()
38
+
39
+
40
+ def _lock_for(name: str) -> threading.Lock:
41
+ with _runtime_locks_guard:
42
+ lock = _runtime_locks.get(name)
43
+ if lock is None:
44
+ lock = threading.Lock()
45
+ _runtime_locks[name] = lock
46
+ return lock
47
+
48
+
49
+ def _is_loopback_host(hostname: str | None) -> bool:
50
+ """True when ``hostname`` is a loopback address, or a name (such as
51
+ ``localhost``) that resolves only to loopback addresses. Resolving
52
+ rather than string-matching ``localhost`` prevents a host/DNS override
53
+ that maps it to a non-loopback address from bypassing the check.
54
+ A name with no resolvable addresses is rejected, not treated as safe."""
55
+ if hostname is None:
56
+ return False
57
+ try:
58
+ return ipaddress.ip_address(hostname).is_loopback
59
+ except ValueError:
60
+ pass
61
+ try:
62
+ resolved = socket.getaddrinfo(hostname, None)
63
+ except OSError:
64
+ return False
65
+ return bool(resolved) and all(ipaddress.ip_address(info[4][0]).is_loopback for info in resolved)
66
+
67
+
68
+ def _require_loopback_endpoint(entry: ManagedServerConfig) -> None:
69
+ """Reject a non-loopback ``endpointTemplate`` host before any port is
70
+ allocated or process started (DES-AISCI-012: "must bind only to
71
+ 127.0.0.1"). Closes jupytermind#66 / CHANGE-019 (ADR-0084); see
72
+ TEST-AISCI-042.
73
+ """
74
+ sample_endpoint = entry.endpoint_template.format(port=0)
75
+ hostname = urlparse(sample_endpoint).hostname
76
+ if not _is_loopback_host(hostname):
77
+ raise classify_mcp_failure(
78
+ entry.name,
79
+ "managed-startup",
80
+ f"endpointTemplate host {hostname!r} is not loopback; "
81
+ "managed servers must bind only to 127.0.0.1",
82
+ )
83
+
84
+
85
+ def _allocate_port() -> int:
86
+ with socket.socket() as sock:
87
+ sock.bind(("127.0.0.1", 0))
88
+ return int(sock.getsockname()[1])
89
+
90
+
91
+ def _healthy(endpoint: str, health_check_path: str) -> bool:
92
+ try:
93
+ with request.urlopen(f"{endpoint}{health_check_path}", timeout=1) as response:
94
+ return response.status == 200
95
+ except (error.URLError, TimeoutError):
96
+ return False
97
+
98
+
99
+ def _stop_process(process: subprocess.Popen) -> None:
100
+ if process.poll() is None:
101
+ process.terminate()
102
+ try:
103
+ process.wait(timeout=2)
104
+ except subprocess.TimeoutExpired:
105
+ process.kill()
106
+ process.wait(timeout=2)
107
+
108
+
109
+ # @id CODE-AISCI-017
110
+ # @implements REQ-AISCI-017
111
+ # @design DES-AISCI-012
112
+ # @id CODE-AISCI-027
113
+ # @implements REQ-AISCI-019
114
+ # @design DES-AISCI-014
115
+ def ensure_managed_server(
116
+ entry: ManagedServerConfig,
117
+ timeout_ms: int = 30000,
118
+ phase: str = "managed-startup",
119
+ ) -> RuntimeInfo:
120
+ """Start a managed server on first use and reuse it for the session."""
121
+ _require_loopback_endpoint(entry)
122
+ with _lock_for(entry.name):
123
+ existing = _RUNTIMES.get(entry.name)
124
+ if existing is not None:
125
+ process, info, health_check_path = existing
126
+ if process.poll() is None and _healthy(info.endpoint, health_check_path):
127
+ return info
128
+ _stop_locked(entry.name)
129
+
130
+ port = _allocate_port()
131
+ endpoint = entry.endpoint_template.format(port=port)
132
+ command = shlex.split(entry.launch_command.format(port=port))
133
+ process = subprocess.Popen(
134
+ command,
135
+ stdout=subprocess.DEVNULL,
136
+ stderr=subprocess.DEVNULL,
137
+ )
138
+ info = RuntimeInfo(pid=process.pid, port=port, endpoint=endpoint)
139
+ deadline = time.monotonic() + timeout_ms / 1000
140
+ while time.monotonic() < deadline:
141
+ # Check our own child is still alive *before* trusting a health
142
+ # response: an external process that stole the allocated port
143
+ # could otherwise answer the health probe on behalf of our dead
144
+ # child process and get cached as a legitimate managed runtime.
145
+ if process.poll() is not None:
146
+ break
147
+ if _healthy(endpoint, entry.health_check_path):
148
+ _RUNTIMES[entry.name] = (process, info, entry.health_check_path)
149
+ return info
150
+ time.sleep(0.1)
151
+ _stop_process(process)
152
+ raise classify_mcp_failure(entry.name, phase, "startup timeout")
153
+
154
+
155
+ def status(server_name: str) -> RuntimeInfo | None:
156
+ """Return current runtime info when the managed server is still running."""
157
+ with _lock_for(server_name):
158
+ existing = _RUNTIMES.get(server_name)
159
+ if existing is None:
160
+ return None
161
+ process, info, health_check_path = existing
162
+ if process.poll() is None and _healthy(info.endpoint, health_check_path):
163
+ return info
164
+ _stop_locked(server_name)
165
+ return None
166
+
167
+
168
+ def stop(server_name: str) -> None:
169
+ """Terminate only the registered managed server for ``server_name``."""
170
+ with _lock_for(server_name):
171
+ _stop_locked(server_name)
172
+
173
+
174
+ def _stop_locked(server_name: str) -> None:
175
+ """Terminate the registered server; caller must already hold its lock."""
176
+ existing = _RUNTIMES.pop(server_name, None)
177
+ if existing is None:
178
+ return
179
+ process, _info, _health_check_path = existing
180
+ _stop_process(process)
@@ -0,0 +1,49 @@
1
+ """Packaging helpers for ai-scientist npm bootstrap regressions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import subprocess
7
+ from collections.abc import Sequence
8
+ from pathlib import Path
9
+
10
+
11
+ # @id CODE-AISCI-027
12
+ # @implements REQ-AISCI-025 REQ-AGENOM-080 REQ-AIMS-080 REQ-ASTRUCT-060
13
+ # @design DES-AISCI-020 DES-AGENOM-080 DES-AIMS-080 DES-ASTRUCT-060
14
+ def load_package_files(package_json_path: str | Path = "package.json") -> list[str]:
15
+ """Return the npm package whitelist entries from package.json."""
16
+ package_json = Path(package_json_path)
17
+ return json.loads(package_json.read_text("utf-8"))["files"]
18
+
19
+
20
+ # @id CODE-AISCI-028
21
+ # @implements REQ-AISCI-025 REQ-AGENOM-080 REQ-AIMS-080 REQ-ASTRUCT-060
22
+ # @design DES-AISCI-020 DES-AGENOM-080 DES-AIMS-080 DES-ASTRUCT-060
23
+ def load_npm_pack_dry_run_paths(project_root: str | Path = ".") -> set[str]:
24
+ """Return packed file paths reported by `npm pack --dry-run --json`."""
25
+ result = subprocess.run(
26
+ ["npm", "pack", "--dry-run", "--json"],
27
+ check=True,
28
+ capture_output=True,
29
+ cwd=Path(project_root),
30
+ text=True,
31
+ )
32
+ payload = json.loads(result.stdout)
33
+ return {entry["path"] for pack in payload for entry in pack["files"]}
34
+
35
+
36
+ # @id CODE-AISCI-029
37
+ # @implements REQ-AISCI-025 REQ-AGENOM-080 REQ-AIMS-080 REQ-ASTRUCT-060
38
+ # @design DES-AISCI-020 DES-AGENOM-080 DES-AIMS-080 DES-ASTRUCT-060
39
+ def iter_skill_python_globs(package_files: Sequence[str]) -> dict[str, str]:
40
+ """Map exact shipped skill entries to expected Python source globs."""
41
+ mappings = {}
42
+ for entry in package_files:
43
+ if not entry.startswith(".github/skills/") or "/" in entry.removeprefix(".github/skills/"):
44
+ continue
45
+ slug = entry.removeprefix(".github/skills/")
46
+ package_dir = Path("src") / slug.replace("-", "_")
47
+ if (package_dir / "__init__.py").exists():
48
+ mappings[entry] = f"{package_dir.as_posix()}/**/*.py"
49
+ return mappings
@@ -0,0 +1,133 @@
1
+ """Top-level ai_scientist dispatch orchestration."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import importlib
6
+ import inspect
7
+
8
+ from ai_scientist.language import detect_language
9
+ from ai_scientist.manifest import (
10
+ load_phase_manifest,
11
+ scan_repo_skill_registry,
12
+ verify_manifest_against_registry,
13
+ )
14
+ from ai_scientist.mcp_gateway import McpGateway
15
+ from ai_scientist.phase_gate import check_gate
16
+ from ai_scientist.phase_state import active_phase
17
+ from ai_scientist.project_handle import resolve_research_project
18
+ from ai_scientist.skill_invocation import DefaultSkillInvoker
19
+
20
+ _PHASE_HINTS = (
21
+ ("research-planning", ("research-planning", "research plan", "研究計画")),
22
+ (
23
+ "literature-review",
24
+ ("literature-review", "literature review", "search the literature", "文献", "先行研究"),
25
+ ),
26
+ ("experimental-design", ("experimental-design", "experimental design", "実験設計")),
27
+ ("data-analysis", ("data-analysis", "analyze", "analyse", "データ分析", "分析して")),
28
+ ("manuscript-writing", ("manuscript-writing", "write the manuscript", "manuscript", "原稿")),
29
+ ("peer-review", ("peer-review", "review the manuscript", "査読")),
30
+ ("reproducibility-check", ("reproducibility-check", "reproducibility", "再現性")),
31
+ ("presentation", ("presentation", "slides", "発表", "スライド")),
32
+ )
33
+
34
+
35
+ def _message(language: str, phase: str) -> str:
36
+ if language == "ja":
37
+ return f"{phase} フェーズを処理しました。"
38
+ return f"Processed the {phase} phase."
39
+
40
+
41
+ def _gateway_for(mcp_client):
42
+ if mcp_client is None:
43
+ return None
44
+ return McpGateway(clients={"default": mcp_client})
45
+
46
+
47
+ # @id CODE-AISCI-025
48
+ # @implements REQ-AISCI-001
49
+ # @design DES-AISCI-001
50
+ def _requested_phase_from_instruction(instruction: str) -> str | None:
51
+ normalized = instruction.casefold()
52
+ for phase, hints in _PHASE_HINTS:
53
+ if any(hint in normalized or hint in instruction for hint in hints):
54
+ return phase
55
+ return None
56
+
57
+
58
+ # @id CODE-AISCI-026
59
+ # @implements REQ-AISCI-023
60
+ # @design DES-AISCI-015
61
+ def _dispatch_phase(
62
+ phase: str,
63
+ manifest: dict,
64
+ handle,
65
+ instruction: str,
66
+ skill_invoker,
67
+ mcp_client,
68
+ ):
69
+ entry = manifest.get(phase)
70
+ if entry is None:
71
+ raise ValueError(f"Unknown phase: {phase}")
72
+ module = importlib.import_module(entry["modulePath"])
73
+ handler = getattr(module, entry["functionName"])
74
+ gateway = _gateway_for(mcp_client)
75
+ available_args = {
76
+ "handle": handle,
77
+ "instruction": instruction,
78
+ "invoker": skill_invoker,
79
+ "mcp_client": mcp_client,
80
+ "gateway": gateway,
81
+ "server_name": "default" if gateway is not None else None,
82
+ }
83
+ signature = inspect.signature(handler)
84
+ call_args = {
85
+ name: value
86
+ for name, value in available_args.items()
87
+ if name in signature.parameters and value is not None
88
+ }
89
+ return handler(**call_args)
90
+
91
+
92
+ # @id CODE-AISCI-014
93
+ # @implements REQ-AISCI-001
94
+ # @design DES-AISCI-001
95
+ def dispatch(
96
+ instruction: str,
97
+ project_name: str,
98
+ requested_phase: str | None = None,
99
+ override_reason: str | None = None,
100
+ projects_root=None,
101
+ skill_invoker=None,
102
+ mcp_client=None,
103
+ ) -> dict:
104
+ """Resolve the project, verify the manifest, gate phases, and dispatch."""
105
+ language = detect_language(instruction)
106
+ handle = resolve_research_project(project_name, projects_root=projects_root)
107
+ manifest = load_phase_manifest()
108
+ verify_manifest_against_registry(manifest, scan_repo_skill_registry())
109
+ current_active = active_phase(handle)
110
+ phase = requested_phase or _requested_phase_from_instruction(instruction) or current_active
111
+ phase = phase or "research-planning"
112
+ if phase != current_active:
113
+ decision = check_gate(
114
+ handle,
115
+ phase,
116
+ override={"reason": override_reason} if override_reason else None,
117
+ )
118
+ if not decision.allowed:
119
+ return {"language": language, "message": decision.message, "phase": phase}
120
+ result = _dispatch_phase(
121
+ phase,
122
+ manifest,
123
+ handle,
124
+ instruction,
125
+ skill_invoker or DefaultSkillInvoker(),
126
+ mcp_client,
127
+ )
128
+ return {
129
+ "language": language,
130
+ "message": _message(language, phase),
131
+ "phase": phase,
132
+ "result": result,
133
+ }