mlspace-plugin 0.2.1__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.
Files changed (53) hide show
  1. mlspace_mcp/__init__.py +3 -0
  2. mlspace_mcp/__main__.py +125 -0
  3. mlspace_mcp/_status.py +18 -0
  4. mlspace_mcp/auth.py +109 -0
  5. mlspace_mcp/bodyspec.py +184 -0
  6. mlspace_mcp/client.py +170 -0
  7. mlspace_mcp/client_setup.py +235 -0
  8. mlspace_mcp/config.py +283 -0
  9. mlspace_mcp/errors.py +241 -0
  10. mlspace_mcp/formatting.py +502 -0
  11. mlspace_mcp/init_cli.py +356 -0
  12. mlspace_mcp/job_hooks.py +242 -0
  13. mlspace_mcp/jsonc.py +90 -0
  14. mlspace_mcp/overview.py +421 -0
  15. mlspace_mcp/registry.py +632 -0
  16. mlspace_mcp/resource_refs.py +188 -0
  17. mlspace_mcp/server.py +190 -0
  18. mlspace_mcp/setup_ui.py +103 -0
  19. mlspace_mcp/skills/mlspace-call-deployed-model-predict/SKILL.md +36 -0
  20. mlspace_mcp/skills/mlspace-deploy-inference-service/SKILL.md +42 -0
  21. mlspace_mcp/skills/mlspace-diagnose-data-transfer/SKILL.md +41 -0
  22. mlspace_mcp/skills/mlspace-diagnose-failed-training-job/SKILL.md +64 -0
  23. mlspace_mcp/skills/mlspace-diagnose-job-launch-rejection/SKILL.md +44 -0
  24. mlspace_mcp/skills/mlspace-diagnose-unhealthy-inference/SKILL.md +40 -0
  25. mlspace_mcp/skills/mlspace-discover-launchable-resources/SKILL.md +45 -0
  26. mlspace_mcp/skills/mlspace-launch-training-job/SKILL.md +66 -0
  27. mlspace_mcp/skills/mlspace-list-running-jobs/SKILL.md +43 -0
  28. mlspace_mcp/skills/mlspace-resume-notebook-and-set-autoshutdown/SKILL.md +44 -0
  29. mlspace_mcp/skills/mlspace-stage-s3-to-nfs/SKILL.md +45 -0
  30. mlspace_mcp/skills/mlspace-transfer-compute-node/SKILL.md +63 -0
  31. mlspace_mcp/spec/body_schemas.json +7506 -0
  32. mlspace_mcp/spec/openapi.json +28347 -0
  33. mlspace_mcp/tls.py +46 -0
  34. mlspace_mcp/tools/__init__.py +1 -0
  35. mlspace_mcp/tools/allocations.py +92 -0
  36. mlspace_mcp/tools/async_inference.py +77 -0
  37. mlspace_mcp/tools/build_image.py +76 -0
  38. mlspace_mcp/tools/dalle.py +52 -0
  39. mlspace_mcp/tools/data_transfer.py +260 -0
  40. mlspace_mcp/tools/docker_registry.py +156 -0
  41. mlspace_mcp/tools/inference.py +88 -0
  42. mlspace_mcp/tools/jobs.py +155 -0
  43. mlspace_mcp/tools/notebooks.py +174 -0
  44. mlspace_mcp/tools/queues.py +202 -0
  45. mlspace_mcp/tools/resources.py +86 -0
  46. mlspace_mcp/tools/tensorboards.py +112 -0
  47. mlspace_mcp/tools/workspaces.py +92 -0
  48. mlspace_mcp/workspace_context.py +107 -0
  49. mlspace_plugin-0.2.1.dist-info/METADATA +427 -0
  50. mlspace_plugin-0.2.1.dist-info/RECORD +53 -0
  51. mlspace_plugin-0.2.1.dist-info/WHEEL +4 -0
  52. mlspace_plugin-0.2.1.dist-info/entry_points.txt +2 -0
  53. mlspace_plugin-0.2.1.dist-info/licenses/LICENSE +21 -0
mlspace_mcp/errors.py ADDED
@@ -0,0 +1,241 @@
1
+ """Structured, user-safe errors and HTTP/exception → message mapping.
2
+
3
+ Our own secrets (Authorization / x-api-key / client_secret) are kept out of error
4
+ text by construction: we never put request headers or the request body into
5
+ messages. We DO surface the API's response body (truncated) on 4xx — under the
6
+ assumption the API does not echo submitted credentials back; if that ever proves
7
+ false for some endpoint, add a scrub pass here.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import re
13
+
14
+ import httpx
15
+
16
+ _BODY_LIMIT = 600
17
+
18
+ # A generic error page detector. Matches only when the body BEGINS with an HTML
19
+ # document/root/title/heading tag — so it never fires on JSON or plain text that
20
+ # merely mentions "<html>" mid-string. Used solely on 4xx bodies (see below).
21
+ _HTML_START = re.compile(r"\s*<(?:!doctype\s+html|html[\s>]|title>|h1>)", re.IGNORECASE)
22
+
23
+
24
+ def _body_as_text(resp: httpx.Response) -> str:
25
+ """Human-readable body text, UNWRAPPING a bare JSON string.
26
+
27
+ Some upstreams return an HTML error page inside a JSON string with a
28
+ ``content-type: application/json`` header; then ``resp.json()`` yields a ``str``
29
+ (not a dict), and that str is the HTML. Fail-open: on any decode trouble fall
30
+ back to ``resp.text`` (then to "")."""
31
+ try:
32
+ parsed = resp.json()
33
+ except Exception:
34
+ parsed = None
35
+ if isinstance(parsed, str):
36
+ return parsed
37
+ try:
38
+ return resp.text or ""
39
+ except Exception:
40
+ return ""
41
+
42
+
43
+ def _is_html_error_page(resp: httpx.Response) -> bool:
44
+ """True if the response body is a generic HTML error page (by content-type or
45
+ by a leading HTML tag). Fail-open: any unexpected shape returns False, which
46
+ keeps the pre-existing (raw-body) behaviour."""
47
+ try:
48
+ ctype = resp.headers.get("content-type", "").lower()
49
+ if "html" in ctype:
50
+ return True
51
+ return bool(_HTML_START.match(_body_as_text(resp)))
52
+ except Exception:
53
+ return False
54
+
55
+
56
+ def _html_note(status: int) -> str:
57
+ """A concise, honest description of a generic HTML error page for ``status``.
58
+
59
+ Names ONLY the response shape and the indistinguishable states it implies —
60
+ it invents no reason the page did not carry. 400/422 wording is a preventive
61
+ fail-safe (no HTML case has been observed there; every observed one is 404,
62
+ plus text/plain 500 which this does not touch)."""
63
+ note = (
64
+ "the server returned a generic HTML error page instead of a structured JSON "
65
+ "error. It carries no machine-readable reason"
66
+ )
67
+ if status == 404:
68
+ note += (
69
+ " and does not distinguish among the possible causes — wrong resource "
70
+ "name, unsupported route, or a resource that is not ready yet. Treat as: "
71
+ "not found by the upstream service."
72
+ )
73
+ elif 500 <= status < 600:
74
+ note += " — the upstream handler failed before producing one."
75
+ else:
76
+ note += "."
77
+ return note
78
+
79
+
80
+ class MLSpaceError(Exception):
81
+ """An error safe to surface to the LLM/user. Tools render it via ``to_text``."""
82
+
83
+ def __init__(self, message: str, *, status: int | None = None, hint: str | None = None):
84
+ self.message = message
85
+ self.status = status
86
+ self.hint = hint
87
+ super().__init__(message)
88
+
89
+ def to_text(self) -> str:
90
+ parts = [f"Error: {self.message}"]
91
+ if self.hint:
92
+ parts.append(self.hint)
93
+ return " ".join(parts)
94
+
95
+
96
+ def _safe_body(resp: httpx.Response) -> str:
97
+ try:
98
+ text = resp.text or ""
99
+ except Exception:
100
+ return ""
101
+ text = text.strip().replace("\n", " ")
102
+ if len(text) > _BODY_LIMIT:
103
+ text = text[:_BODY_LIMIT] + "…"
104
+ return text
105
+
106
+
107
+ def _detail(resp: httpx.Response) -> str:
108
+ """Render an API error body into a concise, actionable message.
109
+
110
+ Handles FastAPI-style ``{"detail": [{"loc": [...], "msg": "..."}]}`` (the
111
+ shape MLSpace returns for validation errors) so the model gets per-field
112
+ reasons it can fix, and falls back to the raw (truncated) body otherwise.
113
+ """
114
+ try:
115
+ data = resp.json()
116
+ except Exception:
117
+ return _safe_body(resp)
118
+ if isinstance(data, dict):
119
+ detail = data.get("detail", data)
120
+ if isinstance(detail, list):
121
+ parts = []
122
+ for item in detail[:8]:
123
+ if isinstance(item, dict):
124
+ loc = [str(p) for p in item.get("loc", []) if p != "body"]
125
+ msg = item.get("msg") or item.get("type") or ""
126
+ parts.append(f"{'.'.join(loc)}: {msg}".strip(": "))
127
+ else:
128
+ parts.append(str(item))
129
+ if parts:
130
+ return "; ".join(parts)
131
+ if isinstance(detail, str):
132
+ return detail
133
+ # other shapes (e.g. {"reason": ...}), possibly nested under "detail"
134
+ scope = detail if isinstance(detail, dict) else data
135
+ for key in ("reason", "message", "error_message"):
136
+ if isinstance(scope.get(key), str) and scope[key]:
137
+ return scope[key]
138
+ return _safe_body(resp)
139
+
140
+
141
+ def error_from_response(resp: httpx.Response) -> MLSpaceError:
142
+ """Map an HTTP error response to an actionable MLSpaceError."""
143
+ status = resp.status_code
144
+ body = _safe_body(resp)
145
+ # Detect a generic HTML error page once (fail-open → False keeps raw-body behaviour).
146
+ is_html = _is_html_error_page(resp)
147
+ if status == 400:
148
+ detail = _html_note(400) if is_html else _detail(resp)
149
+ return MLSpaceError(
150
+ f"Bad request (400). {detail}".strip(),
151
+ status=400,
152
+ hint="Check the action's parameters and body against the API schema.",
153
+ )
154
+ if status == 401:
155
+ return MLSpaceError(
156
+ "Authentication failed (401).",
157
+ status=401,
158
+ hint="Check MLSPACE_CLIENT_ID/MLSPACE_CLIENT_SECRET and MLSPACE_API_KEY.",
159
+ )
160
+ if status == 403:
161
+ return MLSpaceError(
162
+ "Permission denied (403).",
163
+ status=403,
164
+ hint="The service account may lack access to this workspace or resource.",
165
+ )
166
+ if status == 404:
167
+ detail = _html_note(404) if is_html else body
168
+ return MLSpaceError(
169
+ f"Not found (404). {detail}".strip(),
170
+ status=404,
171
+ hint="Check the resource name/id and that it exists in this workspace.",
172
+ )
173
+ if status == 409:
174
+ return MLSpaceError(f"Conflict (409). {body}".strip(), status=409)
175
+ if status == 422:
176
+ detail = _html_note(422) if is_html else _detail(resp)
177
+ return MLSpaceError(
178
+ f"Validation error (422). {detail}".strip(),
179
+ status=422,
180
+ hint="The API rejected the request body or parameters.",
181
+ )
182
+ if status == 429:
183
+ retry_after = resp.headers.get("retry-after")
184
+ return MLSpaceError(
185
+ "Rate limited (429).",
186
+ status=429,
187
+ hint=f"Retry after {retry_after}s." if retry_after else "Slow down and retry shortly.",
188
+ )
189
+ if status == 445:
190
+ # 445 is a non-standard code some WAF/security layers emit; the body is a
191
+ # large HTML page we deliberately keep out of the message/hint entirely. We
192
+ # cannot prove from the status alone that it is a WAF, so we phrase it as a
193
+ # possibility and give the one durable, actionable fact: it is deterministic.
194
+ return MLSpaceError(
195
+ "Request blocked before reaching the platform (HTTP 445) — a non-standard "
196
+ "code used by some WAF/security layers.",
197
+ status=445,
198
+ hint="The request tripped a security rule (e.g. shell metacharacters or a "
199
+ "path-traversal-looking value in some field). This is deterministic — change "
200
+ "the offending input; do not retry the request unchanged.",
201
+ )
202
+ if 500 <= status < 600:
203
+ # Not all MLSpace 5xx are transient: some endpoints fail deterministically
204
+ # for a whole class of input (e.g. allocation assignments on cluster-type
205
+ # allocations 500s every single time, verified live). Advising a blind
206
+ # "retry later" there sends an agent into an unbounded retry loop, so the
207
+ # hint deliberately bounds the retry and names the alternative.
208
+ return MLSpaceError(
209
+ f"MLSpace server error ({status}).",
210
+ status=status,
211
+ hint=(
212
+ "Retry ONCE. If it fails again with the same status, treat it as a "
213
+ "deterministic failure of this endpoint for this input — do not keep "
214
+ "retrying; try a different action or report the endpoint as broken."
215
+ ),
216
+ )
217
+ detail = _html_note(status) if is_html else body
218
+ return MLSpaceError(f"Unexpected HTTP {status}. {detail}".strip(), status=status)
219
+
220
+
221
+ def error_from_exception(exc: Exception) -> MLSpaceError:
222
+ """Map an httpx transport exception to an actionable MLSpaceError."""
223
+ # TimeoutException is the base of Connect/Read/Write/PoolTimeout.
224
+ if isinstance(exc, httpx.TimeoutException):
225
+ return MLSpaceError(
226
+ "Request to the MLSpace API timed out.",
227
+ hint="Retry, or raise MLSPACE_TIMEOUT for slow operations.",
228
+ )
229
+ if isinstance(exc, httpx.ConnectError):
230
+ return MLSpaceError(
231
+ "Could not connect to the MLSpace API.",
232
+ hint="Check network access and MLSPACE_BASE_URL.",
233
+ )
234
+ if isinstance(exc, httpx.RemoteProtocolError):
235
+ return MLSpaceError(
236
+ "Protocol error talking to the MLSpace API.",
237
+ hint="Retry; if it persists the endpoint may be misbehaving.",
238
+ )
239
+ if isinstance(exc, httpx.HTTPError):
240
+ return MLSpaceError(f"HTTP client error ({type(exc).__name__}).")
241
+ return MLSpaceError(f"Unexpected error ({type(exc).__name__}).")