QuickLab-MCP 0.1.0__py3-none-any.whl
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.
- ml_mcp/__init__.py +3 -0
- ml_mcp/application/__init__.py +1 -0
- ml_mcp/application/analysis/__init__.py +15 -0
- ml_mcp/application/analysis/diagnostics.py +122 -0
- ml_mcp/application/analysis/service.py +155 -0
- ml_mcp/application/artifacts/__init__.py +5 -0
- ml_mcp/application/artifacts/service.py +149 -0
- ml_mcp/application/datasets/__init__.py +6 -0
- ml_mcp/application/datasets/service.py +178 -0
- ml_mcp/application/datasets/validator.py +156 -0
- ml_mcp/application/experiments/__init__.py +5 -0
- ml_mcp/application/experiments/service.py +254 -0
- ml_mcp/application/models/__init__.py +6 -0
- ml_mcp/application/models/catalog_seed.py +131 -0
- ml_mcp/application/models/service.py +177 -0
- ml_mcp/application/projects/__init__.py +5 -0
- ml_mcp/application/projects/service.py +93 -0
- ml_mcp/config/__init__.py +5 -0
- ml_mcp/config/settings.py +176 -0
- ml_mcp/domain/__init__.py +1 -0
- ml_mcp/domain/errors/__init__.py +39 -0
- ml_mcp/domain/errors/base.py +155 -0
- ml_mcp/domain/errors/codes.py +23 -0
- ml_mcp/domain/policies/__init__.py +16 -0
- ml_mcp/domain/policies/rbac.py +126 -0
- ml_mcp/domain/policies/tenant_isolation.py +34 -0
- ml_mcp/domain/policies/tool_policy.py +185 -0
- ml_mcp/domain/value_objects/__init__.py +41 -0
- ml_mcp/domain/value_objects/dataset.py +60 -0
- ml_mcp/domain/value_objects/experiment.py +131 -0
- ml_mcp/domain/value_objects/model.py +48 -0
- ml_mcp/infrastructure/__init__.py +1 -0
- ml_mcp/infrastructure/object_storage/__init__.py +5 -0
- ml_mcp/infrastructure/object_storage/s3.py +272 -0
- ml_mcp/infrastructure/postgres/__init__.py +14 -0
- ml_mcp/infrastructure/postgres/base.py +40 -0
- ml_mcp/infrastructure/postgres/models.py +356 -0
- ml_mcp/infrastructure/postgres/repositories/__init__.py +21 -0
- ml_mcp/infrastructure/postgres/repositories/artifacts.py +34 -0
- ml_mcp/infrastructure/postgres/repositories/audit.py +34 -0
- ml_mcp/infrastructure/postgres/repositories/datasets.py +85 -0
- ml_mcp/infrastructure/postgres/repositories/experiments.py +134 -0
- ml_mcp/infrastructure/postgres/repositories/metrics.py +61 -0
- ml_mcp/infrastructure/postgres/repositories/models.py +61 -0
- ml_mcp/infrastructure/postgres/repositories/outbox.py +47 -0
- ml_mcp/infrastructure/postgres/repositories/projects.py +83 -0
- ml_mcp/infrastructure/postgres/session.py +117 -0
- ml_mcp/infrastructure/queue/__init__.py +6 -0
- ml_mcp/infrastructure/queue/outbox_processor.py +67 -0
- ml_mcp/infrastructure/queue/task_queue.py +98 -0
- ml_mcp/infrastructure/redis/__init__.py +11 -0
- ml_mcp/infrastructure/redis/client.py +64 -0
- ml_mcp/infrastructure/redis/rate_limiter.py +129 -0
- ml_mcp/infrastructure/telemetry/__init__.py +23 -0
- ml_mcp/infrastructure/telemetry/logging.py +155 -0
- ml_mcp/infrastructure/telemetry/otel.py +141 -0
- ml_mcp/mcp/__init__.py +1 -0
- ml_mcp/mcp/prompts.py +38 -0
- ml_mcp/mcp/resources.py +35 -0
- ml_mcp/mcp/schemas/__init__.py +1 -0
- ml_mcp/mcp/schemas/tools.py +124 -0
- ml_mcp/mcp/tools/__init__.py +63 -0
- ml_mcp/mcp/tools/analysis.py +37 -0
- ml_mcp/mcp/tools/datasets.py +82 -0
- ml_mcp/mcp/tools/discovery.py +82 -0
- ml_mcp/mcp/tools/experiments.py +96 -0
- ml_mcp/mcp/tools/projects.py +39 -0
- ml_mcp/mcp/tools/results.py +59 -0
- ml_mcp/server/__init__.py +1 -0
- ml_mcp/server/app.py +488 -0
- ml_mcp/server/auth.py +146 -0
- ml_mcp/server/context.py +59 -0
- ml_mcp/server/http.py +183 -0
- ml_mcp/server/middleware.py +121 -0
- ml_mcp/server/stdio.py +42 -0
- ml_mcp/workers/__init__.py +1 -0
- ml_mcp/workers/daemon.py +130 -0
- ml_mcp/workers/metrics.py +110 -0
- ml_mcp/workers/preprocessing.py +170 -0
- ml_mcp/workers/runner.py +250 -0
- ml_mcp/workers/sandbox.py +35 -0
- ml_mcp/workers/trainers/__init__.py +52 -0
- ml_mcp/workers/trainers/base.py +52 -0
- ml_mcp/workers/trainers/catboost_model.py +85 -0
- ml_mcp/workers/trainers/lightgbm_model.py +81 -0
- ml_mcp/workers/trainers/sklearn_models.py +266 -0
- ml_mcp/workers/trainers/xgboost_model.py +82 -0
- quicklab_mcp-0.1.0.dist-info/METADATA +540 -0
- quicklab_mcp-0.1.0.dist-info/RECORD +92 -0
- quicklab_mcp-0.1.0.dist-info/WHEEL +5 -0
- quicklab_mcp-0.1.0.dist-info/entry_points.txt +4 -0
- quicklab_mcp-0.1.0.dist-info/top_level.txt +1 -0
ml_mcp/mcp/prompts.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""MCP Prompts providing guided workflows for agentic experimentation."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def experiment_design_prompt(dataset_description: str, task_type: str) -> str:
|
|
5
|
+
"""Prompt guiding agents to design high-yield ML experiments."""
|
|
6
|
+
return f"""You are the ModelLab ML Experimentation Architect.
|
|
7
|
+
Task: Design a rigorous, leak-free experimentation plan.
|
|
8
|
+
|
|
9
|
+
Dataset Description:
|
|
10
|
+
{dataset_description}
|
|
11
|
+
|
|
12
|
+
Task Type:
|
|
13
|
+
{task_type}
|
|
14
|
+
|
|
15
|
+
Guidelines:
|
|
16
|
+
1. Select 2-3 candidate architectures from approved tabular models: Logistic Regression, Random Forest, XGBoost, LightGBM, CatBoost, Linear SVM, MLP.
|
|
17
|
+
2. Recommend appropriate split strategy (Stratified K-Fold for imbalanced classification, Time Series Split for temporal data).
|
|
18
|
+
3. Specify primary and secondary evaluation metrics (e.g. PR AUC for severe class imbalance, ROC AUC for balanced ranking).
|
|
19
|
+
4. Propose non-overlapping hyperparameter variations to test.
|
|
20
|
+
|
|
21
|
+
Use `list_models` and `get_model` to inspect hyperparameter bounds, then submit via `create_experiment`.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def error_diagnosis_prompt(experiment_id: str, problem_summary: str) -> str:
|
|
26
|
+
"""Prompt assisting agents in diagnosing training regressions or anomalies."""
|
|
27
|
+
return f"""You are the ModelLab Diagnostic Auditor.
|
|
28
|
+
Task: Inspect model performance anomalies for Experiment: {experiment_id}
|
|
29
|
+
|
|
30
|
+
Problem Summary:
|
|
31
|
+
{problem_summary}
|
|
32
|
+
|
|
33
|
+
Step-by-step diagnostic workflow:
|
|
34
|
+
1. Call `get_experiment_metrics` for both train and validation splits to evaluate the generalization gap.
|
|
35
|
+
2. Call `analyze_experiment` to inspect automated overfitting and leakage signals.
|
|
36
|
+
3. Call `analyze_model_errors` to review false positives vs false negatives and residual skew.
|
|
37
|
+
4. Formulate 2 actionable next experiments addressing the root cause.
|
|
38
|
+
"""
|
ml_mcp/mcp/resources.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""MCP Resources exposing system metadata and catalog information."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
from ml_mcp.application.models.service import ModelService
|
|
6
|
+
from ml_mcp.infrastructure.postgres.session import get_db_manager
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
async def read_models_resource() -> str:
|
|
10
|
+
"""Read full catalog of approved models (modellab://models)."""
|
|
11
|
+
async with get_db_manager().session() as sess:
|
|
12
|
+
service = ModelService(sess)
|
|
13
|
+
models = await service.list_models()
|
|
14
|
+
return json.dumps(models, indent=2)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
async def read_model_details_resource(model_id: str) -> str:
|
|
18
|
+
"""Read detailed specification of a model (modellab://models/{model_id})."""
|
|
19
|
+
async with get_db_manager().session() as sess:
|
|
20
|
+
service = ModelService(sess)
|
|
21
|
+
details = await service.get_model(model_id)
|
|
22
|
+
return json.dumps(details, indent=2)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
async def read_experiment_metrics_resource(
|
|
26
|
+
experiment_id: str, tenant_id: str = "default-tenant"
|
|
27
|
+
) -> str:
|
|
28
|
+
"""Read metrics for an experiment (modellab://experiments/{experiment_id}/metrics)."""
|
|
29
|
+
async with get_db_manager().session() as sess:
|
|
30
|
+
from ml_mcp.infrastructure.postgres.repositories.metrics import MetricRepository
|
|
31
|
+
|
|
32
|
+
repo = MetricRepository(sess)
|
|
33
|
+
metrics = await repo.get_metrics(tenant_id, experiment_id)
|
|
34
|
+
payload = {m.metric_name: m.metric_value for m in metrics}
|
|
35
|
+
return json.dumps(payload, indent=2)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""MCP tool and resource schemas."""
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""Strict Pydantic schemas for MCP tool arguments and responses."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
# 1. Discovery Schemas
|
|
9
|
+
class ListModelsInput(BaseModel):
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class GetModelInput(BaseModel):
|
|
14
|
+
model_id: str = Field(..., description="Unique model identifier, e.g. 'random_forest'")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ListModelVersionsInput(BaseModel):
|
|
18
|
+
model_id: str = Field(..., description="Unique model identifier")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ListDatasetsInput(BaseModel):
|
|
22
|
+
project_id: str | None = Field(default=None, description="Optional project filter")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class GetDatasetInput(BaseModel):
|
|
26
|
+
dataset_id: str = Field(..., description="Unique dataset identifier")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ListDatasetVersionsInput(BaseModel):
|
|
30
|
+
dataset_id: str = Field(..., description="Unique dataset identifier")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# 2. Dataset Management Schemas
|
|
34
|
+
class RegisterDatasetInput(BaseModel):
|
|
35
|
+
project_id: str = Field(..., description="Project ID to register dataset under")
|
|
36
|
+
name: str = Field(..., description="Human-readable dataset name")
|
|
37
|
+
description: str = Field(default="", description="Detailed description")
|
|
38
|
+
format: str = Field(..., description="Format: 'csv', 'parquet', or 'jsonl'")
|
|
39
|
+
data_base64: str = Field(..., description="Base64-encoded dataset file content")
|
|
40
|
+
version: str = Field(default="1.0", description="Semantic dataset version")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class ValidateDatasetInput(BaseModel):
|
|
44
|
+
format: str = Field(..., description="Format: 'csv', 'parquet', or 'jsonl'")
|
|
45
|
+
data_base64: str = Field(..., description="Base64-encoded dataset file content")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class InspectDatasetInput(BaseModel):
|
|
49
|
+
dataset_id: str = Field(..., description="Dataset ID to inspect")
|
|
50
|
+
version: str = Field(default="1.0", description="Dataset version")
|
|
51
|
+
sample_rows: int = Field(
|
|
52
|
+
default=5, ge=1, le=50, description="Number of preview records to return"
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# 3. Experiment Schemas
|
|
57
|
+
class CreateExperimentInput(BaseModel):
|
|
58
|
+
project_id: str = Field(..., description="Target project ID")
|
|
59
|
+
dataset_version_id: str = Field(..., description="Registered DatasetVersion ID")
|
|
60
|
+
model_version_id: str = Field(..., description="Registered ModelVersion ID")
|
|
61
|
+
task_type: str = Field(
|
|
62
|
+
..., description="'binary_classification', 'multiclass_classification', or 'regression'"
|
|
63
|
+
)
|
|
64
|
+
target_column: str = Field(..., description="Name of column to predict")
|
|
65
|
+
feature_columns: list[str] | None = Field(
|
|
66
|
+
default=None, description="Subset of features to train on"
|
|
67
|
+
)
|
|
68
|
+
primary_metric: str = Field(default="roc_auc", description="Primary optimization metric")
|
|
69
|
+
additional_metrics: list[str] = Field(default_factory=list)
|
|
70
|
+
hyperparameters: dict[str, Any] = Field(default_factory=dict)
|
|
71
|
+
random_seed: int = Field(default=42)
|
|
72
|
+
idempotency_key: str | None = Field(default=None)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class GetExperimentInput(BaseModel):
|
|
76
|
+
experiment_id: str = Field(..., description="Experiment ID to fetch")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class CancelExperimentInput(BaseModel):
|
|
80
|
+
experiment_id: str = Field(..., description="Experiment ID to cancel")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class ListExperimentsInput(BaseModel):
|
|
84
|
+
project_id: str | None = None
|
|
85
|
+
status: str | None = None
|
|
86
|
+
limit: int = Field(default=50, ge=1, le=100)
|
|
87
|
+
offset: int = Field(default=0, ge=0)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
# 4. Results Schemas
|
|
91
|
+
class GetExperimentMetricsInput(BaseModel):
|
|
92
|
+
experiment_id: str = Field(..., description="Target experiment ID")
|
|
93
|
+
split: str | None = Field(default=None, description="'train' or 'validation'")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class GetExperimentPredictionsInput(BaseModel):
|
|
97
|
+
experiment_id: str = Field(..., description="Target experiment ID")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class ListExperimentArtifactsInput(BaseModel):
|
|
101
|
+
experiment_id: str = Field(..., description="Target experiment ID")
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class ReadExperimentArtifactInput(BaseModel):
|
|
105
|
+
artifact_id: str = Field(..., description="Target artifact ID")
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class CompareExperimentsInput(BaseModel):
|
|
109
|
+
experiment_ids: list[str] = Field(
|
|
110
|
+
..., min_length=2, max_length=10, description="List of experiment IDs to compare"
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
# 5. Analysis Schemas
|
|
115
|
+
class AnalyzeExperimentInput(BaseModel):
|
|
116
|
+
experiment_id: str = Field(..., description="Experiment ID to analyze")
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class AnalyzeModelErrorsInput(BaseModel):
|
|
120
|
+
experiment_id: str = Field(..., description="Experiment ID to inspect errors for")
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class CheckExperimentValidityInput(BaseModel):
|
|
124
|
+
experiment_id: str = Field(..., description="Experiment ID to validate")
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""MCP tools module."""
|
|
2
|
+
|
|
3
|
+
from ml_mcp.mcp.tools.analysis import (
|
|
4
|
+
handle_analyze_experiment,
|
|
5
|
+
handle_analyze_model_errors,
|
|
6
|
+
handle_check_experiment_validity,
|
|
7
|
+
)
|
|
8
|
+
from ml_mcp.mcp.tools.datasets import (
|
|
9
|
+
handle_inspect_dataset,
|
|
10
|
+
handle_register_dataset,
|
|
11
|
+
handle_validate_dataset,
|
|
12
|
+
)
|
|
13
|
+
from ml_mcp.mcp.tools.discovery import (
|
|
14
|
+
handle_get_dataset,
|
|
15
|
+
handle_get_model,
|
|
16
|
+
handle_list_dataset_versions,
|
|
17
|
+
handle_list_datasets,
|
|
18
|
+
handle_list_model_versions,
|
|
19
|
+
handle_list_models,
|
|
20
|
+
)
|
|
21
|
+
from ml_mcp.mcp.tools.experiments import (
|
|
22
|
+
handle_cancel_experiment,
|
|
23
|
+
handle_create_experiment,
|
|
24
|
+
handle_get_experiment,
|
|
25
|
+
handle_list_experiments,
|
|
26
|
+
)
|
|
27
|
+
from ml_mcp.mcp.tools.projects import (
|
|
28
|
+
handle_create_project,
|
|
29
|
+
handle_list_projects,
|
|
30
|
+
)
|
|
31
|
+
from ml_mcp.mcp.tools.results import (
|
|
32
|
+
handle_compare_experiments,
|
|
33
|
+
handle_get_experiment_metrics,
|
|
34
|
+
handle_get_experiment_predictions,
|
|
35
|
+
handle_list_experiment_artifacts,
|
|
36
|
+
handle_read_experiment_artifact,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
__all__ = [
|
|
40
|
+
"handle_create_project",
|
|
41
|
+
"handle_list_projects",
|
|
42
|
+
"handle_list_models",
|
|
43
|
+
"handle_get_model",
|
|
44
|
+
"handle_list_model_versions",
|
|
45
|
+
"handle_list_datasets",
|
|
46
|
+
"handle_get_dataset",
|
|
47
|
+
"handle_list_dataset_versions",
|
|
48
|
+
"handle_register_dataset",
|
|
49
|
+
"handle_validate_dataset",
|
|
50
|
+
"handle_inspect_dataset",
|
|
51
|
+
"handle_create_experiment",
|
|
52
|
+
"handle_get_experiment",
|
|
53
|
+
"handle_cancel_experiment",
|
|
54
|
+
"handle_list_experiments",
|
|
55
|
+
"handle_get_experiment_metrics",
|
|
56
|
+
"handle_get_experiment_predictions",
|
|
57
|
+
"handle_list_experiment_artifacts",
|
|
58
|
+
"handle_read_experiment_artifact",
|
|
59
|
+
"handle_compare_experiments",
|
|
60
|
+
"handle_analyze_experiment",
|
|
61
|
+
"handle_analyze_model_errors",
|
|
62
|
+
"handle_check_experiment_validity",
|
|
63
|
+
]
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Analysis MCP tool handlers."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from ml_mcp.application.analysis.service import AnalysisService
|
|
6
|
+
from ml_mcp.domain.policies import Principal, Scope
|
|
7
|
+
from ml_mcp.infrastructure.postgres.session import get_db_manager
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
async def handle_analyze_experiment(
|
|
11
|
+
experiment_id: str,
|
|
12
|
+
principal: Principal,
|
|
13
|
+
) -> dict[str, Any]:
|
|
14
|
+
principal.enforce_permission(Scope.ANALYSIS_CREATE)
|
|
15
|
+
async with get_db_manager().session() as sess:
|
|
16
|
+
service = AnalysisService(sess)
|
|
17
|
+
return await service.analyze_experiment(principal.tenant_id, experiment_id)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
async def handle_analyze_model_errors(
|
|
21
|
+
experiment_id: str,
|
|
22
|
+
principal: Principal,
|
|
23
|
+
) -> dict[str, Any]:
|
|
24
|
+
principal.enforce_permission(Scope.ANALYSIS_CREATE)
|
|
25
|
+
async with get_db_manager().session() as sess:
|
|
26
|
+
service = AnalysisService(sess)
|
|
27
|
+
return await service.analyze_model_errors(principal.tenant_id, experiment_id)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
async def handle_check_experiment_validity(
|
|
31
|
+
experiment_id: str,
|
|
32
|
+
principal: Principal,
|
|
33
|
+
) -> dict[str, Any]:
|
|
34
|
+
principal.enforce_permission(Scope.ANALYSIS_CREATE)
|
|
35
|
+
async with get_db_manager().session() as sess:
|
|
36
|
+
service = AnalysisService(sess)
|
|
37
|
+
return await service.check_experiment_validity(principal.tenant_id, experiment_id)
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import binascii
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from ml_mcp.application.datasets.service import DatasetService
|
|
6
|
+
from ml_mcp.domain.errors import InvalidInputError
|
|
7
|
+
from ml_mcp.domain.policies import Principal, Scope, validate_tenant_access
|
|
8
|
+
from ml_mcp.infrastructure.postgres.session import get_db_manager
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _safe_b64decode(data_base64: str) -> bytes:
|
|
12
|
+
"""Safely decode base64 payload with strict validation."""
|
|
13
|
+
clean_b64 = data_base64.strip()
|
|
14
|
+
if not clean_b64:
|
|
15
|
+
raise InvalidInputError("Dataset payload is empty.")
|
|
16
|
+
try:
|
|
17
|
+
return base64.b64decode(clean_b64, validate=True)
|
|
18
|
+
except (binascii.Error, ValueError) as exc:
|
|
19
|
+
raise InvalidInputError(f"Invalid base64 payload: {exc}") from exc
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
async def handle_register_dataset(
|
|
23
|
+
project_id: str,
|
|
24
|
+
name: str,
|
|
25
|
+
description: str,
|
|
26
|
+
format: str,
|
|
27
|
+
data_base64: str,
|
|
28
|
+
principal: Principal,
|
|
29
|
+
version: str = "1.0",
|
|
30
|
+
) -> dict[str, Any]:
|
|
31
|
+
principal.enforce_permission(Scope.DATASETS_WRITE)
|
|
32
|
+
validate_tenant_access(principal, principal.tenant_id, project_id)
|
|
33
|
+
|
|
34
|
+
raw_bytes = _safe_b64decode(data_base64)
|
|
35
|
+
async with get_db_manager().session() as sess:
|
|
36
|
+
from ml_mcp.domain.errors import ResourceNotFoundError
|
|
37
|
+
from ml_mcp.infrastructure.postgres.repositories.projects import ProjectRepository
|
|
38
|
+
|
|
39
|
+
proj_repo = ProjectRepository(sess)
|
|
40
|
+
proj = await proj_repo.get_project(principal.tenant_id, project_id)
|
|
41
|
+
if not proj:
|
|
42
|
+
raise ResourceNotFoundError("Project", project_id)
|
|
43
|
+
|
|
44
|
+
service = DatasetService(sess)
|
|
45
|
+
return await service.register_dataset(
|
|
46
|
+
tenant_id=principal.tenant_id,
|
|
47
|
+
project_id=project_id,
|
|
48
|
+
name=name,
|
|
49
|
+
description=description,
|
|
50
|
+
data_format=format,
|
|
51
|
+
raw_bytes=raw_bytes,
|
|
52
|
+
version=version,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
async def handle_validate_dataset(
|
|
57
|
+
format: str,
|
|
58
|
+
data_base64: str,
|
|
59
|
+
principal: Principal,
|
|
60
|
+
) -> dict[str, Any]:
|
|
61
|
+
principal.enforce_permission(Scope.DATASETS_READ)
|
|
62
|
+
raw_bytes = _safe_b64decode(data_base64)
|
|
63
|
+
async with get_db_manager().session() as sess:
|
|
64
|
+
service = DatasetService(sess)
|
|
65
|
+
return await service.validate_only(raw_bytes, format)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
async def handle_inspect_dataset(
|
|
69
|
+
dataset_id: str,
|
|
70
|
+
principal: Principal,
|
|
71
|
+
version: str = "1.0",
|
|
72
|
+
sample_rows: int = 5,
|
|
73
|
+
) -> dict[str, Any]:
|
|
74
|
+
principal.enforce_permission(Scope.DATASETS_READ)
|
|
75
|
+
async with get_db_manager().session() as sess:
|
|
76
|
+
service = DatasetService(sess)
|
|
77
|
+
return await service.inspect_dataset(
|
|
78
|
+
tenant_id=principal.tenant_id,
|
|
79
|
+
dataset_id=dataset_id,
|
|
80
|
+
version=version,
|
|
81
|
+
sample_rows=sample_rows,
|
|
82
|
+
)
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""Discovery MCP tools for models and datasets."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from ml_mcp.application.datasets.service import DatasetService
|
|
6
|
+
from ml_mcp.application.models.service import ModelService
|
|
7
|
+
from ml_mcp.domain.policies import Principal, Scope
|
|
8
|
+
from ml_mcp.infrastructure.postgres.session import get_db_manager
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
async def handle_list_models(principal: Principal) -> list[dict[str, Any]]:
|
|
12
|
+
principal.enforce_permission(Scope.MODELS_READ)
|
|
13
|
+
async with get_db_manager().session() as sess:
|
|
14
|
+
service = ModelService(sess)
|
|
15
|
+
return await service.list_models()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
async def handle_get_model(model_id: str, principal: Principal) -> dict[str, Any]:
|
|
19
|
+
principal.enforce_permission(Scope.MODELS_READ)
|
|
20
|
+
async with get_db_manager().session() as sess:
|
|
21
|
+
service = ModelService(sess)
|
|
22
|
+
return await service.get_model(model_id)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
async def handle_list_model_versions(model_id: str, principal: Principal) -> list[dict[str, Any]]:
|
|
26
|
+
principal.enforce_permission(Scope.MODELS_READ)
|
|
27
|
+
async with get_db_manager().session() as sess:
|
|
28
|
+
service = ModelService(sess)
|
|
29
|
+
return await service.list_model_versions(model_id)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
async def handle_list_datasets(
|
|
33
|
+
principal: Principal, project_id: str | None = None
|
|
34
|
+
) -> list[dict[str, Any]]:
|
|
35
|
+
principal.enforce_permission(Scope.DATASETS_READ)
|
|
36
|
+
if project_id:
|
|
37
|
+
from ml_mcp.domain.policies import validate_tenant_access
|
|
38
|
+
|
|
39
|
+
validate_tenant_access(principal, principal.tenant_id, project_id)
|
|
40
|
+
|
|
41
|
+
async with get_db_manager().session() as sess:
|
|
42
|
+
if project_id:
|
|
43
|
+
from ml_mcp.domain.errors import ResourceNotFoundError
|
|
44
|
+
from ml_mcp.infrastructure.postgres.repositories.projects import ProjectRepository
|
|
45
|
+
|
|
46
|
+
proj_repo = ProjectRepository(sess)
|
|
47
|
+
proj = await proj_repo.get_project(principal.tenant_id, project_id)
|
|
48
|
+
if not proj:
|
|
49
|
+
raise ResourceNotFoundError("Project", project_id)
|
|
50
|
+
|
|
51
|
+
service = DatasetService(sess)
|
|
52
|
+
return await service.list_datasets(principal.tenant_id, project_id)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
async def handle_get_dataset(dataset_id: str, principal: Principal) -> dict[str, Any]:
|
|
56
|
+
principal.enforce_permission(Scope.DATASETS_READ)
|
|
57
|
+
async with get_db_manager().session() as sess:
|
|
58
|
+
service = DatasetService(sess)
|
|
59
|
+
return await service.get_dataset(principal.tenant_id, dataset_id)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
async def handle_list_dataset_versions(
|
|
63
|
+
dataset_id: str, principal: Principal
|
|
64
|
+
) -> list[dict[str, Any]]:
|
|
65
|
+
principal.enforce_permission(Scope.DATASETS_READ)
|
|
66
|
+
async with get_db_manager().session() as sess:
|
|
67
|
+
from ml_mcp.infrastructure.postgres.repositories.datasets import DatasetRepository
|
|
68
|
+
|
|
69
|
+
repo = DatasetRepository(sess)
|
|
70
|
+
versions = await repo.list_versions(principal.tenant_id, dataset_id)
|
|
71
|
+
return [
|
|
72
|
+
{
|
|
73
|
+
"version_id": v.id,
|
|
74
|
+
"version": v.version,
|
|
75
|
+
"row_count": v.row_count,
|
|
76
|
+
"column_count": v.column_count,
|
|
77
|
+
"content_hash": v.content_hash,
|
|
78
|
+
"validation_status": v.validation_status,
|
|
79
|
+
"created_at": v.created_at.isoformat(),
|
|
80
|
+
}
|
|
81
|
+
for v in versions
|
|
82
|
+
]
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""Experiment MCP tool handlers."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from ml_mcp.application.experiments.service import ExperimentService
|
|
6
|
+
from ml_mcp.domain.policies import Principal, Scope, validate_tenant_access
|
|
7
|
+
from ml_mcp.infrastructure.postgres.session import get_db_manager
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
async def handle_create_experiment(
|
|
11
|
+
project_id: str,
|
|
12
|
+
dataset_version_id: str,
|
|
13
|
+
model_version_id: str,
|
|
14
|
+
task_type: str,
|
|
15
|
+
target_column: str,
|
|
16
|
+
principal: Principal,
|
|
17
|
+
feature_columns: list[str] | None = None,
|
|
18
|
+
primary_metric: str = "roc_auc",
|
|
19
|
+
additional_metrics: list[str] | None = None,
|
|
20
|
+
hyperparameters: dict[str, Any] | None = None,
|
|
21
|
+
random_seed: int = 42,
|
|
22
|
+
idempotency_key: str | None = None,
|
|
23
|
+
) -> dict[str, Any]:
|
|
24
|
+
principal.enforce_permission(Scope.EXPERIMENTS_CREATE)
|
|
25
|
+
validate_tenant_access(principal, principal.tenant_id, project_id)
|
|
26
|
+
|
|
27
|
+
async with get_db_manager().session() as sess:
|
|
28
|
+
from ml_mcp.domain.errors import ResourceNotFoundError
|
|
29
|
+
from ml_mcp.infrastructure.postgres.repositories.projects import ProjectRepository
|
|
30
|
+
|
|
31
|
+
proj_repo = ProjectRepository(sess)
|
|
32
|
+
proj = await proj_repo.get_project(principal.tenant_id, project_id)
|
|
33
|
+
if not proj:
|
|
34
|
+
raise ResourceNotFoundError("Project", project_id)
|
|
35
|
+
|
|
36
|
+
service = ExperimentService(sess)
|
|
37
|
+
return await service.create_experiment(
|
|
38
|
+
tenant_id=principal.tenant_id,
|
|
39
|
+
project_id=project_id,
|
|
40
|
+
dataset_version_id=dataset_version_id,
|
|
41
|
+
model_version_id=model_version_id,
|
|
42
|
+
task_type=task_type,
|
|
43
|
+
target_column=target_column,
|
|
44
|
+
principal_id=principal.principal_id,
|
|
45
|
+
feature_columns=feature_columns,
|
|
46
|
+
primary_metric=primary_metric,
|
|
47
|
+
additional_metrics=additional_metrics,
|
|
48
|
+
hyperparameters=hyperparameters,
|
|
49
|
+
random_seed=random_seed,
|
|
50
|
+
idempotency_key=idempotency_key,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
async def handle_get_experiment(experiment_id: str, principal: Principal) -> dict[str, Any]:
|
|
55
|
+
principal.enforce_permission(Scope.EXPERIMENTS_READ)
|
|
56
|
+
async with get_db_manager().session() as sess:
|
|
57
|
+
service = ExperimentService(sess)
|
|
58
|
+
return await service.get_experiment(principal.tenant_id, experiment_id)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
async def handle_cancel_experiment(experiment_id: str, principal: Principal) -> dict[str, Any]:
|
|
62
|
+
principal.enforce_permission(Scope.EXPERIMENTS_CANCEL)
|
|
63
|
+
async with get_db_manager().session() as sess:
|
|
64
|
+
service = ExperimentService(sess)
|
|
65
|
+
return await service.cancel_experiment(principal.tenant_id, experiment_id)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
async def handle_list_experiments(
|
|
69
|
+
principal: Principal,
|
|
70
|
+
project_id: str | None = None,
|
|
71
|
+
status: str | None = None,
|
|
72
|
+
limit: int = 50,
|
|
73
|
+
offset: int = 0,
|
|
74
|
+
) -> list[dict[str, Any]]:
|
|
75
|
+
principal.enforce_permission(Scope.EXPERIMENTS_READ)
|
|
76
|
+
if project_id:
|
|
77
|
+
validate_tenant_access(principal, principal.tenant_id, project_id)
|
|
78
|
+
|
|
79
|
+
async with get_db_manager().session() as sess:
|
|
80
|
+
if project_id:
|
|
81
|
+
from ml_mcp.domain.errors import ResourceNotFoundError
|
|
82
|
+
from ml_mcp.infrastructure.postgres.repositories.projects import ProjectRepository
|
|
83
|
+
|
|
84
|
+
proj_repo = ProjectRepository(sess)
|
|
85
|
+
proj = await proj_repo.get_project(principal.tenant_id, project_id)
|
|
86
|
+
if not proj:
|
|
87
|
+
raise ResourceNotFoundError("Project", project_id)
|
|
88
|
+
|
|
89
|
+
service = ExperimentService(sess)
|
|
90
|
+
return await service.list_experiments(
|
|
91
|
+
tenant_id=principal.tenant_id,
|
|
92
|
+
project_id=project_id,
|
|
93
|
+
status=status,
|
|
94
|
+
limit=limit,
|
|
95
|
+
offset=offset,
|
|
96
|
+
)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Project MCP tool handlers."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from ml_mcp.application.projects.service import ProjectService
|
|
6
|
+
from ml_mcp.domain.policies import Principal, Scope
|
|
7
|
+
from ml_mcp.infrastructure.postgres.session import get_db_manager
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
async def handle_create_project(
|
|
11
|
+
name: str,
|
|
12
|
+
principal: Principal,
|
|
13
|
+
description: str = "",
|
|
14
|
+
) -> dict[str, Any]:
|
|
15
|
+
"""Handle create_project tool call."""
|
|
16
|
+
principal.enforce_permission(Scope.PROJECTS_WRITE)
|
|
17
|
+
async with get_db_manager().session() as sess:
|
|
18
|
+
service = ProjectService(sess)
|
|
19
|
+
return await service.create_project(
|
|
20
|
+
tenant_id=principal.tenant_id,
|
|
21
|
+
name=name,
|
|
22
|
+
description=description,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
async def handle_list_projects(
|
|
27
|
+
principal: Principal,
|
|
28
|
+
limit: int = 50,
|
|
29
|
+
offset: int = 0,
|
|
30
|
+
) -> list[dict[str, Any]]:
|
|
31
|
+
"""Handle list_projects tool call."""
|
|
32
|
+
principal.enforce_permission(Scope.PROJECTS_READ)
|
|
33
|
+
async with get_db_manager().session() as sess:
|
|
34
|
+
service = ProjectService(sess)
|
|
35
|
+
return await service.list_projects(
|
|
36
|
+
tenant_id=principal.tenant_id,
|
|
37
|
+
limit=limit,
|
|
38
|
+
offset=offset,
|
|
39
|
+
)
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Result and artifact MCP tool handlers."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from ml_mcp.application.artifacts.service import ArtifactService
|
|
6
|
+
from ml_mcp.domain.policies import Principal, Scope
|
|
7
|
+
from ml_mcp.infrastructure.postgres.session import get_db_manager
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
async def handle_get_experiment_metrics(
|
|
11
|
+
experiment_id: str,
|
|
12
|
+
principal: Principal,
|
|
13
|
+
split: str | None = None,
|
|
14
|
+
) -> dict[str, Any]:
|
|
15
|
+
principal.enforce_permission(Scope.EXPERIMENTS_READ)
|
|
16
|
+
async with get_db_manager().session() as sess:
|
|
17
|
+
service = ArtifactService(sess)
|
|
18
|
+
return await service.get_metrics(principal.tenant_id, experiment_id, split)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
async def handle_get_experiment_predictions(
|
|
22
|
+
experiment_id: str,
|
|
23
|
+
principal: Principal,
|
|
24
|
+
limit: int = 5,
|
|
25
|
+
) -> dict[str, Any]:
|
|
26
|
+
principal.enforce_permission(Scope.ARTIFACTS_READ)
|
|
27
|
+
async with get_db_manager().session() as sess:
|
|
28
|
+
service = ArtifactService(sess)
|
|
29
|
+
return await service.get_predictions(principal.tenant_id, experiment_id, limit)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
async def handle_list_experiment_artifacts(
|
|
33
|
+
experiment_id: str,
|
|
34
|
+
principal: Principal,
|
|
35
|
+
) -> list[dict[str, Any]]:
|
|
36
|
+
principal.enforce_permission(Scope.ARTIFACTS_READ)
|
|
37
|
+
async with get_db_manager().session() as sess:
|
|
38
|
+
service = ArtifactService(sess)
|
|
39
|
+
return await service.list_artifacts(principal.tenant_id, experiment_id)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
async def handle_read_experiment_artifact(
|
|
43
|
+
artifact_id: str,
|
|
44
|
+
principal: Principal,
|
|
45
|
+
) -> dict[str, Any]:
|
|
46
|
+
principal.enforce_permission(Scope.ARTIFACTS_READ)
|
|
47
|
+
async with get_db_manager().session() as sess:
|
|
48
|
+
service = ArtifactService(sess)
|
|
49
|
+
return await service.get_artifact(principal.tenant_id, artifact_id)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
async def handle_compare_experiments(
|
|
53
|
+
experiment_ids: list[str],
|
|
54
|
+
principal: Principal,
|
|
55
|
+
) -> dict[str, Any]:
|
|
56
|
+
principal.enforce_permission(Scope.EXPERIMENTS_READ)
|
|
57
|
+
async with get_db_manager().session() as sess:
|
|
58
|
+
service = ArtifactService(sess)
|
|
59
|
+
return await service.compare_experiments(principal.tenant_id, experiment_ids)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Server package for ML-MCP."""
|