databricks-mason 0.1.2.dev0__tar.gz → 0.1.3.dev0__tar.gz
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.
- {databricks_mason-0.1.2.dev0 → databricks_mason-0.1.3.dev0}/PKG-INFO +24 -11
- {databricks_mason-0.1.2.dev0 → databricks_mason-0.1.3.dev0}/README.md +22 -9
- {databricks_mason-0.1.2.dev0 → databricks_mason-0.1.3.dev0}/pyproject.toml +2 -2
- {databricks_mason-0.1.2.dev0 → databricks_mason-0.1.3.dev0}/src/databricks_mason/agent_project.py +60 -1
- databricks_mason-0.1.3.dev0/src/databricks_mason/auth.py +139 -0
- {databricks_mason-0.1.2.dev0 → databricks_mason-0.1.3.dev0}/src/databricks_mason/client.py +61 -13
- {databricks_mason-0.1.2.dev0 → databricks_mason-0.1.3.dev0}/src/databricks_mason/deploy.py +56 -25
- {databricks_mason-0.1.2.dev0 → databricks_mason-0.1.3.dev0}/src/databricks_mason/dev.py +16 -2
- {databricks_mason-0.1.2.dev0 → databricks_mason-0.1.3.dev0}/src/databricks_mason/errors.py +30 -2
- databricks_mason-0.1.3.dev0/src/databricks_mason/help.py +282 -0
- {databricks_mason-0.1.2.dev0 → databricks_mason-0.1.3.dev0}/src/databricks_mason/init.py +5 -5
- {databricks_mason-0.1.2.dev0 → databricks_mason-0.1.3.dev0}/src/databricks_mason/langgraph/__init__.py +1 -5
- databricks_mason-0.1.3.dev0/src/databricks_mason/langgraph/memory.py +67 -0
- {databricks_mason-0.1.2.dev0 → databricks_mason-0.1.3.dev0}/src/databricks_mason/langgraph/session_store.py +12 -12
- {databricks_mason-0.1.2.dev0 → databricks_mason-0.1.3.dev0}/src/databricks_mason/memory.py +86 -3
- {databricks_mason-0.1.2.dev0 → databricks_mason-0.1.3.dev0}/src/databricks_mason/openai/__init__.py +1 -5
- databricks_mason-0.1.3.dev0/src/databricks_mason/openai/memory.py +67 -0
- {databricks_mason-0.1.2.dev0 → databricks_mason-0.1.3.dev0}/src/databricks_mason/openai/sessions.py +12 -14
- {databricks_mason-0.1.2.dev0 → databricks_mason-0.1.3.dev0}/src/databricks_mason/render.py +51 -5
- {databricks_mason-0.1.2.dev0 → databricks_mason-0.1.3.dev0}/src/databricks_mason/runtime/tool_manifest.py +47 -0
- {databricks_mason-0.1.2.dev0 → databricks_mason-0.1.3.dev0}/src/databricks_mason/sessions.py +91 -5
- {databricks_mason-0.1.2.dev0 → databricks_mason-0.1.3.dev0}/src/databricks_mason/tools.py +46 -0
- {databricks_mason-0.1.2.dev0 → databricks_mason-0.1.3.dev0}/src/databricks_mason/tracing.py +19 -6
- databricks_mason-0.1.2.dev0/src/databricks_mason/auth.py +0 -83
- databricks_mason-0.1.2.dev0/src/databricks_mason/help.py +0 -139
- databricks_mason-0.1.2.dev0/src/databricks_mason/langgraph/memory.py +0 -63
- databricks_mason-0.1.2.dev0/src/databricks_mason/openai/memory.py +0 -63
- {databricks_mason-0.1.2.dev0 → databricks_mason-0.1.3.dev0}/.gitignore +0 -0
- {databricks_mason-0.1.2.dev0 → databricks_mason-0.1.3.dev0}/NOTICE +0 -0
- {databricks_mason-0.1.2.dev0 → databricks_mason-0.1.3.dev0}/src/databricks_mason/__init__.py +0 -0
- {databricks_mason-0.1.2.dev0 → databricks_mason-0.1.3.dev0}/src/databricks_mason/cli.py +0 -0
- {databricks_mason-0.1.2.dev0 → databricks_mason-0.1.3.dev0}/src/databricks_mason/langgraph/mcp.py +0 -0
- {databricks_mason-0.1.2.dev0 → databricks_mason-0.1.3.dev0}/src/databricks_mason/mcp.py +0 -0
- {databricks_mason-0.1.2.dev0 → databricks_mason-0.1.3.dev0}/src/databricks_mason/memory_store_access.py +0 -0
- {databricks_mason-0.1.2.dev0 → databricks_mason-0.1.3.dev0}/src/databricks_mason/models.py +0 -0
- {databricks_mason-0.1.2.dev0 → databricks_mason-0.1.3.dev0}/src/databricks_mason/openai/mcp.py +0 -0
- {databricks_mason-0.1.2.dev0 → databricks_mason-0.1.3.dev0}/src/databricks_mason/project_config.py +0 -0
- {databricks_mason-0.1.2.dev0 → databricks_mason-0.1.3.dev0}/src/databricks_mason/py.typed +0 -0
- {databricks_mason-0.1.2.dev0 → databricks_mason-0.1.3.dev0}/src/databricks_mason/runtime/__init__.py +0 -0
- {databricks_mason-0.1.2.dev0 → databricks_mason-0.1.3.dev0}/src/databricks_mason/runtime/background.py +0 -0
- {databricks_mason-0.1.2.dev0 → databricks_mason-0.1.3.dev0}/src/databricks_mason/runtime/session_store_client.py +0 -0
- {databricks_mason-0.1.2.dev0 → databricks_mason-0.1.3.dev0}/src/databricks_mason/runtime/tracing.py +0 -0
- {databricks_mason-0.1.2.dev0 → databricks_mason-0.1.3.dev0}/src/databricks_mason/runtime/workspace.py +0 -0
- {databricks_mason-0.1.2.dev0 → databricks_mason-0.1.3.dev0}/src/databricks_mason/sandbox.py +0 -0
- {databricks_mason-0.1.2.dev0 → databricks_mason-0.1.3.dev0}/src/databricks_mason/session_store_access.py +0 -0
- {databricks_mason-0.1.2.dev0 → databricks_mason-0.1.3.dev0}/src/databricks_mason/store_access.py +0 -0
- {databricks_mason-0.1.2.dev0 → databricks_mason-0.1.3.dev0}/src/databricks_mason/templates/python_tool_langgraph.py +0 -0
- {databricks_mason-0.1.2.dev0 → databricks_mason-0.1.3.dev0}/src/databricks_mason/templates/python_tool_test.py +0 -0
- {databricks_mason-0.1.2.dev0 → databricks_mason-0.1.3.dev0}/src/databricks_mason/templates/sandbox_mcp.py +0 -0
- {databricks_mason-0.1.2.dev0 → databricks_mason-0.1.3.dev0}/src/databricks_mason/templates/sandbox_mcp_langgraph.py +0 -0
- {databricks_mason-0.1.2.dev0 → databricks_mason-0.1.3.dev0}/src/databricks_mason/timefmt.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: databricks-mason
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.3.dev0
|
|
4
4
|
Summary: Databricks integration for Mason
|
|
5
5
|
Author-email: Databricks <agent-feedback@databricks.com>
|
|
6
6
|
License-File: NOTICE
|
|
@@ -27,7 +27,7 @@ Requires-Dist: databricks-agents>=1.9.3; extra == 'runtime-openai'
|
|
|
27
27
|
Requires-Dist: databricks-openai>=0.13.0; extra == 'runtime-openai'
|
|
28
28
|
Requires-Dist: fastapi>=0.129.0; extra == 'runtime-openai'
|
|
29
29
|
Requires-Dist: mlflow>=3.10.1; extra == 'runtime-openai'
|
|
30
|
-
Requires-Dist: openai-agents>=0.
|
|
30
|
+
Requires-Dist: openai-agents>=0.7.0; extra == 'runtime-openai'
|
|
31
31
|
Requires-Dist: opentelemetry-exporter-otlp-proto-grpc>=1.25.0; extra == 'runtime-openai'
|
|
32
32
|
Requires-Dist: uuid-utils>=0.10.0; extra == 'runtime-openai'
|
|
33
33
|
Provides-Extra: tracing
|
|
@@ -62,23 +62,31 @@ For tracing commands, install Mason with tracing extras:
|
|
|
62
62
|
pip install 'databricks-mason[tracing]'
|
|
63
63
|
```
|
|
64
64
|
|
|
65
|
+
## Shell completion
|
|
66
|
+
Add this to `~/.zshrc`:
|
|
67
|
+
```sh
|
|
68
|
+
eval "$(_MASON_COMPLETE=zsh_source mason)"
|
|
69
|
+
```
|
|
70
|
+
|
|
65
71
|
## Authentication
|
|
66
72
|
|
|
67
73
|
Mason uses [Databricks authentication](https://docs.databricks.com/aws/en/dev-tools/cli/authentication).
|
|
68
|
-
|
|
69
|
-
then ask Mason to validate and remember that profile:
|
|
74
|
+
Ask Mason to authenticate and remember a named profile:
|
|
70
75
|
|
|
71
76
|
```sh
|
|
72
|
-
databricks auth login --profile <profile>
|
|
73
77
|
mason login --profile <profile>
|
|
74
78
|
mason sessions stores list
|
|
75
79
|
```
|
|
76
80
|
|
|
77
|
-
`mason login`
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
81
|
+
`mason login` validates existing credentials first. If credentials are missing or rejected in
|
|
82
|
+
an interactive terminal, Mason runs `databricks auth login --profile <profile>`, revalidates the
|
|
83
|
+
profile, and stores the selection in `~/.mason/config.json`. This browser-based setup requires
|
|
84
|
+
the Databricks CLI. In non-interactive environments, authenticate the profile before running
|
|
85
|
+
Mason. `mason logout` forgets the saved selection without revoking the underlying credentials.
|
|
86
|
+
|
|
87
|
+
If Databricks SDK default authentication is already configured, you can skip `mason login`.
|
|
88
|
+
You can also pass the global `--profile/-p` option before an individual command, for example
|
|
89
|
+
`mason --profile <profile> mcp list`. Use `--output json` for scripting.
|
|
82
90
|
|
|
83
91
|
## Python SDK
|
|
84
92
|
|
|
@@ -153,7 +161,7 @@ mason sessions items append --help
|
|
|
153
161
|
For the shortest path from a blank directory to a running and deployed agent:
|
|
154
162
|
|
|
155
163
|
```sh
|
|
156
|
-
mason login --profile
|
|
164
|
+
mason login --profile <profile>
|
|
157
165
|
mason init my-agent
|
|
158
166
|
cd my-agent
|
|
159
167
|
mason dev
|
|
@@ -176,9 +184,14 @@ mason tools add sandbox --scope table:samples.nyctaxi.trips
|
|
|
176
184
|
mason tools add mcp system.ai.web_search
|
|
177
185
|
mason tools add uc-function catalog.schema.lookup_ticket
|
|
178
186
|
mason tools add python lookup-ticket
|
|
187
|
+
mason tools remove mcp system.ai.web_search
|
|
179
188
|
mason tools list
|
|
180
189
|
```
|
|
181
190
|
|
|
191
|
+
For MCP services, the remove command accepts the same service name as the add command. You can also
|
|
192
|
+
remove any binding by the ID shown in `mason tools list`, for example `mason tools remove
|
|
193
|
+
web_search`. Removal updates only `agent.toml`; Python source and test files remain user-owned.
|
|
194
|
+
|
|
182
195
|
Discover the MCP Services available to your user before adding one. By default Mason lists the
|
|
183
196
|
Databricks-managed services in `system.ai`; pass `--schema catalog.schema` for another Unity Catalog
|
|
184
197
|
schema. Text output includes a copyable add command, while `--output json` returns normalized service
|
|
@@ -26,23 +26,31 @@ For tracing commands, install Mason with tracing extras:
|
|
|
26
26
|
pip install 'databricks-mason[tracing]'
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
+
## Shell completion
|
|
30
|
+
Add this to `~/.zshrc`:
|
|
31
|
+
```sh
|
|
32
|
+
eval "$(_MASON_COMPLETE=zsh_source mason)"
|
|
33
|
+
```
|
|
34
|
+
|
|
29
35
|
## Authentication
|
|
30
36
|
|
|
31
37
|
Mason uses [Databricks authentication](https://docs.databricks.com/aws/en/dev-tools/cli/authentication).
|
|
32
|
-
|
|
33
|
-
then ask Mason to validate and remember that profile:
|
|
38
|
+
Ask Mason to authenticate and remember a named profile:
|
|
34
39
|
|
|
35
40
|
```sh
|
|
36
|
-
databricks auth login --profile <profile>
|
|
37
41
|
mason login --profile <profile>
|
|
38
42
|
mason sessions stores list
|
|
39
43
|
```
|
|
40
44
|
|
|
41
|
-
`mason login`
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
45
|
+
`mason login` validates existing credentials first. If credentials are missing or rejected in
|
|
46
|
+
an interactive terminal, Mason runs `databricks auth login --profile <profile>`, revalidates the
|
|
47
|
+
profile, and stores the selection in `~/.mason/config.json`. This browser-based setup requires
|
|
48
|
+
the Databricks CLI. In non-interactive environments, authenticate the profile before running
|
|
49
|
+
Mason. `mason logout` forgets the saved selection without revoking the underlying credentials.
|
|
50
|
+
|
|
51
|
+
If Databricks SDK default authentication is already configured, you can skip `mason login`.
|
|
52
|
+
You can also pass the global `--profile/-p` option before an individual command, for example
|
|
53
|
+
`mason --profile <profile> mcp list`. Use `--output json` for scripting.
|
|
46
54
|
|
|
47
55
|
## Python SDK
|
|
48
56
|
|
|
@@ -117,7 +125,7 @@ mason sessions items append --help
|
|
|
117
125
|
For the shortest path from a blank directory to a running and deployed agent:
|
|
118
126
|
|
|
119
127
|
```sh
|
|
120
|
-
mason login --profile
|
|
128
|
+
mason login --profile <profile>
|
|
121
129
|
mason init my-agent
|
|
122
130
|
cd my-agent
|
|
123
131
|
mason dev
|
|
@@ -140,9 +148,14 @@ mason tools add sandbox --scope table:samples.nyctaxi.trips
|
|
|
140
148
|
mason tools add mcp system.ai.web_search
|
|
141
149
|
mason tools add uc-function catalog.schema.lookup_ticket
|
|
142
150
|
mason tools add python lookup-ticket
|
|
151
|
+
mason tools remove mcp system.ai.web_search
|
|
143
152
|
mason tools list
|
|
144
153
|
```
|
|
145
154
|
|
|
155
|
+
For MCP services, the remove command accepts the same service name as the add command. You can also
|
|
156
|
+
remove any binding by the ID shown in `mason tools list`, for example `mason tools remove
|
|
157
|
+
web_search`. Removal updates only `agent.toml`; Python source and test files remain user-owned.
|
|
158
|
+
|
|
146
159
|
Discover the MCP Services available to your user before adding one. By default Mason lists the
|
|
147
160
|
Databricks-managed services in `system.ai`; pass `--schema catalog.schema` for another Unity Catalog
|
|
148
161
|
schema. Text output includes a copyable add command, while `--output json` returns normalized service
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "databricks-mason"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.3.dev0"
|
|
4
4
|
description = "Databricks integration for Mason"
|
|
5
5
|
authors = [
|
|
6
6
|
{ name="Databricks", email="agent-feedback@databricks.com" },
|
|
@@ -38,7 +38,7 @@ runtime = [
|
|
|
38
38
|
"databricks-agents>=1.9.3",
|
|
39
39
|
]
|
|
40
40
|
runtime-openai = [
|
|
41
|
-
"openai-agents>=0.
|
|
41
|
+
"openai-agents>=0.7.0",
|
|
42
42
|
"databricks-openai>=0.13.0",
|
|
43
43
|
"fastapi>=0.129.0",
|
|
44
44
|
"mlflow>=3.10.1",
|
{databricks_mason-0.1.2.dev0 → databricks_mason-0.1.3.dev0}/src/databricks_mason/agent_project.py
RENAMED
|
@@ -15,6 +15,7 @@ from tomlkit import TOMLDocument
|
|
|
15
15
|
from tomlkit.exceptions import ParseError
|
|
16
16
|
|
|
17
17
|
from databricks_mason.errors import AgentCliError
|
|
18
|
+
from databricks_mason.runtime.tool_manifest import MEMORY_STORE_TABLE, SESSION_STORE_TABLE
|
|
18
19
|
|
|
19
20
|
_SCHEMA_VERSION = 1
|
|
20
21
|
_SUPPORTED_FRAMEWORKS = {"langgraph", "openai"}
|
|
@@ -191,6 +192,15 @@ def _required_string(value: object, description: str) -> str:
|
|
|
191
192
|
return value
|
|
192
193
|
|
|
193
194
|
|
|
195
|
+
def _store_name_from_manifest(value: object, table: str) -> str | None:
|
|
196
|
+
"""Read the ``name`` from a ``[memory_store]`` / ``[session_store]`` table, or None if absent."""
|
|
197
|
+
if value is None:
|
|
198
|
+
return None
|
|
199
|
+
if not isinstance(value, Mapping):
|
|
200
|
+
raise AgentCliError(f"agent.toml [{table}] must be a table.")
|
|
201
|
+
return _required_string(cast(Mapping[str, Any], value).get("name"), f"[{table}] name")
|
|
202
|
+
|
|
203
|
+
|
|
194
204
|
def _scope_from_manifest(value: object) -> Scope:
|
|
195
205
|
if not isinstance(value, Mapping):
|
|
196
206
|
raise AgentCliError("Sandbox downscope entries must be TOML tables.")
|
|
@@ -275,12 +285,17 @@ class AgentProject:
|
|
|
275
285
|
document: TOMLDocument,
|
|
276
286
|
framework: str,
|
|
277
287
|
tools: list[ToolSpec],
|
|
288
|
+
memory_store: str | None = None,
|
|
289
|
+
session_store: str | None = None,
|
|
278
290
|
) -> None:
|
|
279
291
|
self.root = root
|
|
280
292
|
self.path = root / "agent.toml"
|
|
281
293
|
self._document = document
|
|
282
294
|
self.framework = framework
|
|
283
295
|
self.tools = tools
|
|
296
|
+
# Managed store bindings declared in agent.toml; None = unbound.
|
|
297
|
+
self.memory_store = memory_store
|
|
298
|
+
self.session_store = session_store
|
|
284
299
|
|
|
285
300
|
@classmethod
|
|
286
301
|
def load(cls, root: pathlib.Path | str) -> "AgentProject":
|
|
@@ -314,7 +329,13 @@ class AgentProject:
|
|
|
314
329
|
ids = [tool.id for tool in tools]
|
|
315
330
|
if len(ids) != len(set(ids)):
|
|
316
331
|
raise AgentCliError("agent.toml tool ids must be unique.")
|
|
317
|
-
|
|
332
|
+
memory_store = _store_name_from_manifest(
|
|
333
|
+
document.get(MEMORY_STORE_TABLE), MEMORY_STORE_TABLE
|
|
334
|
+
)
|
|
335
|
+
session_store = _store_name_from_manifest(
|
|
336
|
+
document.get(SESSION_STORE_TABLE), SESSION_STORE_TABLE
|
|
337
|
+
)
|
|
338
|
+
return cls(project_root, document, framework, tools, memory_store, session_store)
|
|
318
339
|
|
|
319
340
|
@classmethod
|
|
320
341
|
def create(cls, root: pathlib.Path | str, *, framework: str) -> "AgentProject":
|
|
@@ -367,6 +388,44 @@ class AgentProject:
|
|
|
367
388
|
del self.tools[index]
|
|
368
389
|
return True
|
|
369
390
|
|
|
391
|
+
def bind_memory_store(self, name: str) -> bool:
|
|
392
|
+
"""Declare the memory store binding in agent.toml. Returns True if it changed."""
|
|
393
|
+
return self._set_store(MEMORY_STORE_TABLE, name)
|
|
394
|
+
|
|
395
|
+
def bind_session_store(self, name: str) -> bool:
|
|
396
|
+
"""Declare the session store binding in agent.toml. Returns True if it changed."""
|
|
397
|
+
return self._set_store(SESSION_STORE_TABLE, name)
|
|
398
|
+
|
|
399
|
+
def unbind_memory_store(self) -> bool:
|
|
400
|
+
"""Remove the memory store binding from agent.toml. Returns True if it was present."""
|
|
401
|
+
return self._clear_store(MEMORY_STORE_TABLE)
|
|
402
|
+
|
|
403
|
+
def unbind_session_store(self) -> bool:
|
|
404
|
+
"""Remove the session store binding from agent.toml. Returns True if it was present."""
|
|
405
|
+
return self._clear_store(SESSION_STORE_TABLE)
|
|
406
|
+
|
|
407
|
+
def _set_store(self, table: str, name: str) -> bool:
|
|
408
|
+
name = _required_string(name, f"[{table}] name")
|
|
409
|
+
if getattr(self, table) == name:
|
|
410
|
+
return False
|
|
411
|
+
existing = self._document.get(table)
|
|
412
|
+
if isinstance(existing, Mapping):
|
|
413
|
+
existing["name"] = name
|
|
414
|
+
else:
|
|
415
|
+
store_table = tomlkit.table()
|
|
416
|
+
store_table.add("name", name)
|
|
417
|
+
self._document.append(table, store_table)
|
|
418
|
+
setattr(self, table, name)
|
|
419
|
+
return True
|
|
420
|
+
|
|
421
|
+
def _clear_store(self, table: str) -> bool:
|
|
422
|
+
if getattr(self, table) is None:
|
|
423
|
+
return False
|
|
424
|
+
if table in self._document:
|
|
425
|
+
del self._document[table]
|
|
426
|
+
setattr(self, table, None)
|
|
427
|
+
return True
|
|
428
|
+
|
|
370
429
|
def write(self) -> pathlib.Path:
|
|
371
430
|
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
372
431
|
temporary: pathlib.Path | None = None
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""`mason login` / `logout` — remember an optional Databricks profile.
|
|
2
|
+
|
|
3
|
+
`login` validates a named profile and persists the selection; when credentials are missing or
|
|
4
|
+
rejected in an interactive terminal, it delegates setup to `databricks auth login` and retries.
|
|
5
|
+
The root group falls back to the saved profile whenever `-p` is omitted. Without a saved profile,
|
|
6
|
+
the Databricks SDK performs its normal default authentication resolution. `logout` removes only
|
|
7
|
+
Mason's saved selection, not the underlying credentials. State lives in a small JSON file under
|
|
8
|
+
`~/.mason` (override the directory with `MASON_CONFIG_HOME`, mainly for tests).
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import os
|
|
15
|
+
import pathlib
|
|
16
|
+
import subprocess
|
|
17
|
+
import sys
|
|
18
|
+
from typing import Optional
|
|
19
|
+
|
|
20
|
+
import click
|
|
21
|
+
from databricks.sdk.errors import Unauthenticated
|
|
22
|
+
|
|
23
|
+
from databricks_mason import render
|
|
24
|
+
from databricks_mason.client import MasonClient
|
|
25
|
+
from databricks_mason.errors import AgentCliError
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _config_file() -> pathlib.Path:
|
|
29
|
+
base = os.environ.get("MASON_CONFIG_HOME")
|
|
30
|
+
root = pathlib.Path(base) if base else pathlib.Path.home() / ".mason"
|
|
31
|
+
return root / "config.json"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def load_default_profile() -> Optional[str]:
|
|
35
|
+
"""The profile saved by `mason login`, or None if the user never logged in."""
|
|
36
|
+
try:
|
|
37
|
+
return json.loads(_config_file().read_text()).get("profile")
|
|
38
|
+
except (OSError, json.JSONDecodeError):
|
|
39
|
+
return None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _save_default_profile(profile: str) -> None:
|
|
43
|
+
path = _config_file()
|
|
44
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
45
|
+
path.write_text(json.dumps({"profile": profile}, indent=2) + "\n")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _validate_profile(profile: str) -> tuple[MasonClient, str]:
|
|
49
|
+
client = MasonClient(profile)
|
|
50
|
+
return client, client.current_user
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _is_interactive() -> bool:
|
|
54
|
+
return sys.stdin.isatty()
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _run_databricks_login(profile: str) -> None:
|
|
58
|
+
command = ["databricks", "auth", "login", "--profile", profile]
|
|
59
|
+
try:
|
|
60
|
+
# Keep the child process interactive while preserving stdout for Mason's JSON output.
|
|
61
|
+
result = subprocess.run(command, text=True, check=False, stdout=sys.stderr)
|
|
62
|
+
except FileNotFoundError as exc:
|
|
63
|
+
raise AgentCliError(
|
|
64
|
+
"Could not configure Databricks authentication: the `databricks` CLI was not found.",
|
|
65
|
+
hint=f"Install the Databricks CLI, then retry `mason login --profile {profile}`.",
|
|
66
|
+
) from exc
|
|
67
|
+
if result.returncode != 0:
|
|
68
|
+
raise AgentCliError(
|
|
69
|
+
f"`databricks auth login --profile {profile}` failed (exit {result.returncode})."
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _authenticate_profile(profile: str) -> tuple[MasonClient, str]:
|
|
74
|
+
try:
|
|
75
|
+
return _validate_profile(profile)
|
|
76
|
+
except (AgentCliError, Unauthenticated) as initial_error:
|
|
77
|
+
if not _is_interactive():
|
|
78
|
+
raise AgentCliError(
|
|
79
|
+
f"Could not validate Databricks profile {profile!r}: {initial_error}",
|
|
80
|
+
hint="Run this command in an interactive terminal so Mason can open "
|
|
81
|
+
"Databricks login, or authenticate first with "
|
|
82
|
+
f"`databricks auth login --profile {profile}`.",
|
|
83
|
+
) from initial_error
|
|
84
|
+
except Exception as validation_error: # noqa: BLE001 - normalize unexpected API failures
|
|
85
|
+
raise AgentCliError(
|
|
86
|
+
f"Could not validate Databricks profile {profile!r}: {validation_error}"
|
|
87
|
+
) from validation_error
|
|
88
|
+
|
|
89
|
+
_run_databricks_login(profile)
|
|
90
|
+
try:
|
|
91
|
+
return _validate_profile(profile)
|
|
92
|
+
except Exception as retry_error: # noqa: BLE001 - normalize the post-login failure
|
|
93
|
+
raise AgentCliError(
|
|
94
|
+
f"Databricks login completed, but profile {profile!r} could not be validated: "
|
|
95
|
+
f"{retry_error}"
|
|
96
|
+
) from retry_error
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@click.command()
|
|
100
|
+
@click.option(
|
|
101
|
+
"--profile",
|
|
102
|
+
"-p",
|
|
103
|
+
default=None,
|
|
104
|
+
help="Profile to authenticate with and remember as the default.",
|
|
105
|
+
)
|
|
106
|
+
@click.pass_obj
|
|
107
|
+
def login(obj, profile) -> None:
|
|
108
|
+
"""Authenticate a profile and save it as the default, so later commands can omit -p."""
|
|
109
|
+
profile = profile or obj.profile
|
|
110
|
+
if not profile:
|
|
111
|
+
raise AgentCliError(
|
|
112
|
+
"No profile to save.",
|
|
113
|
+
hint="Pass one to remember, e.g. `mason login --profile <profile>`.",
|
|
114
|
+
)
|
|
115
|
+
client, user = _authenticate_profile(profile)
|
|
116
|
+
_save_default_profile(profile)
|
|
117
|
+
if obj.output == "json":
|
|
118
|
+
render.emit_json({"profile": profile, "user": user, "host": client.host})
|
|
119
|
+
return
|
|
120
|
+
render.success(
|
|
121
|
+
f"Logged in as {user}",
|
|
122
|
+
fields={"Profile": profile, "Host": client.host},
|
|
123
|
+
next_steps=[
|
|
124
|
+
("mason init my-agent", "Scaffold a new agent project"),
|
|
125
|
+
],
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@click.command()
|
|
130
|
+
@click.pass_obj
|
|
131
|
+
def logout(obj) -> None:
|
|
132
|
+
"""Forget the saved profile selection without deleting its credentials."""
|
|
133
|
+
path = _config_file()
|
|
134
|
+
existed = path.exists()
|
|
135
|
+
path.unlink(missing_ok=True)
|
|
136
|
+
if obj.output == "json":
|
|
137
|
+
render.emit_json({"logged_out": existed})
|
|
138
|
+
return
|
|
139
|
+
render.success("Logged out" if existed else "No saved login to clear")
|
|
@@ -14,16 +14,23 @@ from __future__ import annotations
|
|
|
14
14
|
import configparser
|
|
15
15
|
import os
|
|
16
16
|
import pathlib
|
|
17
|
+
import time
|
|
17
18
|
from typing import Any, Optional
|
|
18
19
|
|
|
19
20
|
from databricks.sdk import WorkspaceClient
|
|
20
21
|
|
|
21
22
|
from databricks_mason import models
|
|
22
|
-
from databricks_mason.errors import AgentCliError, wrap_api_error
|
|
23
|
+
from databricks_mason.errors import TRANSIENT_ERROR_CODES, AgentCliError, wrap_api_error
|
|
23
24
|
|
|
24
25
|
_BASE = "/api/agents/v1"
|
|
25
26
|
_MCP_SERVICES_PATH = "/api/2.1/unity-catalog/mcp-services"
|
|
26
27
|
|
|
28
|
+
# Transient backend failures (e.g. a CANCELLED RPC) usually clear on a retry, so retry safe
|
|
29
|
+
# requests once before surfacing them. Mutating requests must opt in explicitly: their first
|
|
30
|
+
# attempt may have committed even when its response was lost.
|
|
31
|
+
_MAX_ATTEMPTS = 2
|
|
32
|
+
_RETRY_BASE_DELAY_S = 0.2
|
|
33
|
+
|
|
27
34
|
|
|
28
35
|
def _query(**kwargs: Any) -> dict[str, Any]:
|
|
29
36
|
"""Build a query dict, dropping None and empty values."""
|
|
@@ -118,8 +125,8 @@ class MasonClient:
|
|
|
118
125
|
except Exception as exc: # noqa: BLE001 - surfaced as a clean CLI error
|
|
119
126
|
raise AgentCliError(
|
|
120
127
|
f"Could not initialize Databricks auth: {exc}",
|
|
121
|
-
hint="
|
|
122
|
-
"or
|
|
128
|
+
hint="Select an existing profile with `mason --profile <name> <command>` "
|
|
129
|
+
"or authenticate and save it with `mason login --profile <name>`.",
|
|
123
130
|
) from exc
|
|
124
131
|
|
|
125
132
|
@property
|
|
@@ -140,12 +147,27 @@ class MasonClient:
|
|
|
140
147
|
self._w.workspace.mkdirs(path)
|
|
141
148
|
|
|
142
149
|
def _do(
|
|
143
|
-
self,
|
|
150
|
+
self,
|
|
151
|
+
method: str,
|
|
152
|
+
path: str,
|
|
153
|
+
*,
|
|
154
|
+
query: Optional[dict] = None,
|
|
155
|
+
body: Optional[dict] = None,
|
|
156
|
+
safe_to_retry: bool = False,
|
|
144
157
|
) -> Any:
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
158
|
+
retry_allowed = method == "GET" or safe_to_retry
|
|
159
|
+
delay = _RETRY_BASE_DELAY_S
|
|
160
|
+
for attempt in range(1, _MAX_ATTEMPTS + 1):
|
|
161
|
+
try:
|
|
162
|
+
return self._w.api_client.do(method, path, query=query, body=body)
|
|
163
|
+
except Exception as exc: # noqa: BLE001 - normalized to AgentCliError
|
|
164
|
+
retryable = (
|
|
165
|
+
retry_allowed and getattr(exc, "error_code", None) in TRANSIENT_ERROR_CODES
|
|
166
|
+
)
|
|
167
|
+
if not retryable or attempt == _MAX_ATTEMPTS:
|
|
168
|
+
raise wrap_api_error(exc) from exc
|
|
169
|
+
time.sleep(delay)
|
|
170
|
+
delay *= 2
|
|
149
171
|
|
|
150
172
|
# --- Unity Catalog MCP Services -----------------------------------------
|
|
151
173
|
|
|
@@ -162,10 +184,22 @@ class MasonClient:
|
|
|
162
184
|
# --- memory stores -------------------------------------------------------
|
|
163
185
|
|
|
164
186
|
def create_memory_store(
|
|
165
|
-
self,
|
|
187
|
+
self,
|
|
188
|
+
display_name: str,
|
|
189
|
+
description: Optional[str] = None,
|
|
190
|
+
*,
|
|
191
|
+
retry_transient: bool = False,
|
|
166
192
|
) -> models.MemoryStore:
|
|
167
193
|
body = _query(display_name=display_name, description=description)
|
|
168
|
-
return _as(
|
|
194
|
+
return _as(
|
|
195
|
+
models.MemoryStore,
|
|
196
|
+
self._do(
|
|
197
|
+
"POST",
|
|
198
|
+
f"{_BASE}/memory-stores",
|
|
199
|
+
body=body,
|
|
200
|
+
safe_to_retry=retry_transient,
|
|
201
|
+
),
|
|
202
|
+
)
|
|
169
203
|
|
|
170
204
|
def get_memory_store(self, name: str) -> models.MemoryStore:
|
|
171
205
|
return _as(models.MemoryStore, self._do("GET", f"{_BASE}/{memory_store_path(name)}"))
|
|
@@ -262,7 +296,12 @@ class MasonClient:
|
|
|
262
296
|
body = _query(actor_id=actor_id, query=query, limit=limit)
|
|
263
297
|
return _as(
|
|
264
298
|
models.MemorySearchResult,
|
|
265
|
-
self._do(
|
|
299
|
+
self._do(
|
|
300
|
+
"POST",
|
|
301
|
+
f"{_BASE}/{memory_store_path(store)}/entries:search",
|
|
302
|
+
body=body,
|
|
303
|
+
safe_to_retry=True,
|
|
304
|
+
),
|
|
266
305
|
)
|
|
267
306
|
|
|
268
307
|
def update_memory_entry(
|
|
@@ -286,13 +325,22 @@ class MasonClient:
|
|
|
286
325
|
# --- session stores ------------------------------------------------------
|
|
287
326
|
|
|
288
327
|
def create_session_store(
|
|
289
|
-
self,
|
|
328
|
+
self,
|
|
329
|
+
name: str,
|
|
330
|
+
description: Optional[str] = None,
|
|
331
|
+
metadata: Optional[dict] = None,
|
|
332
|
+
*,
|
|
333
|
+
retry_transient: bool = False,
|
|
290
334
|
) -> models.SessionStore:
|
|
291
335
|
body = _query(description=description, metadata=metadata)
|
|
292
336
|
return _as(
|
|
293
337
|
models.SessionStore,
|
|
294
338
|
self._do(
|
|
295
|
-
"POST",
|
|
339
|
+
"POST",
|
|
340
|
+
f"{_BASE}/session-stores",
|
|
341
|
+
query={"session_store_name": name},
|
|
342
|
+
body=body,
|
|
343
|
+
safe_to_retry=retry_transient,
|
|
296
344
|
),
|
|
297
345
|
)
|
|
298
346
|
|