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,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
|
+
}
|