databricks-agentbricks 0.2.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.
- databricks_agentbricks/__init__.py +1 -0
- databricks_agentbricks/_group.py +217 -0
- databricks_agentbricks/agent_project.py +657 -0
- databricks_agentbricks/app_resources.py +139 -0
- databricks_agentbricks/cli/__init__.py +12 -0
- databricks_agentbricks/cli/app.py +108 -0
- databricks_agentbricks/cli/app_auth.py +198 -0
- databricks_agentbricks/cli/auth.py +142 -0
- databricks_agentbricks/cli/deploy.py +954 -0
- databricks_agentbricks/cli/dev.py +310 -0
- databricks_agentbricks/cli/endpoint.py +145 -0
- databricks_agentbricks/cli/endpoint_examples.py +33 -0
- databricks_agentbricks/cli/endpoint_output.py +53 -0
- databricks_agentbricks/cli/endpoint_request.py +71 -0
- databricks_agentbricks/cli/endpoint_transport.py +148 -0
- databricks_agentbricks/cli/help.py +467 -0
- databricks_agentbricks/cli/init.py +496 -0
- databricks_agentbricks/cli/mcp.py +75 -0
- databricks_agentbricks/cli/memory.py +520 -0
- databricks_agentbricks/cli/sandbox.py +823 -0
- databricks_agentbricks/cli/sessions.py +588 -0
- databricks_agentbricks/cli/tools.py +456 -0
- databricks_agentbricks/cli/tracing.py +724 -0
- databricks_agentbricks/databricks_cli.py +38 -0
- databricks_agentbricks/errors.py +114 -0
- databricks_agentbricks/lakebase_runtime_store.py +107 -0
- databricks_agentbricks/legacy_lakebase_runtime_store.py +74 -0
- databricks_agentbricks/project_config.py +157 -0
- databricks_agentbricks/project_types.py +55 -0
- databricks_agentbricks/py.typed +0 -0
- databricks_agentbricks/render.py +349 -0
- databricks_agentbricks/templates/agent-bricks-migrate/SKILL.md +101 -0
- databricks_agentbricks/templates/agent-langgraph/.env.example +36 -0
- databricks_agentbricks/templates/agent-langgraph/.gitignore +199 -0
- databricks_agentbricks/templates/agent-langgraph/AGENTKIT_CONTRACT.md +95 -0
- databricks_agentbricks/templates/agent-langgraph/AGENTS.md +31 -0
- databricks_agentbricks/templates/agent-langgraph/CLAUDE.md +1 -0
- databricks_agentbricks/templates/agent-langgraph/README.md +141 -0
- databricks_agentbricks/templates/agent-langgraph/agent/__init__.py +0 -0
- databricks_agentbricks/templates/agent-langgraph/agent/agent.py +177 -0
- databricks_agentbricks/templates/agent-langgraph/agent/mcps.py +27 -0
- databricks_agentbricks/templates/agent-langgraph/agent/tools/__init__.py +24 -0
- databricks_agentbricks/templates/agent-langgraph/agent/tools/sample_tool.py +15 -0
- databricks_agentbricks/templates/agent-langgraph/agent/tools/send_message.py +17 -0
- databricks_agentbricks/templates/agent-langgraph/app.yaml +15 -0
- databricks_agentbricks/templates/agent-langgraph/pyproject.toml +42 -0
- databricks_agentbricks/templates/agent-langgraph/runtime/__init__.py +0 -0
- databricks_agentbricks/templates/agent-langgraph/runtime/adapter.py +144 -0
- databricks_agentbricks/templates/agent-langgraph/runtime/main.py +25 -0
- databricks_agentbricks/templates/agent-langgraph/tests/__init__.py +0 -0
- databricks_agentbricks/templates/agent-langgraph/tests/test_agent.py +211 -0
- databricks_agentbricks/templates/agent-langgraph/tests/test_runtime.py +19 -0
- databricks_agentbricks/templates/agent-langgraph/tests/test_workspace.py +28 -0
- databricks_agentbricks/templates/agent-openai/.env.example +37 -0
- databricks_agentbricks/templates/agent-openai/.gitignore +199 -0
- databricks_agentbricks/templates/agent-openai/AGENTKIT_CONTRACT.md +107 -0
- databricks_agentbricks/templates/agent-openai/AGENTS.md +76 -0
- databricks_agentbricks/templates/agent-openai/CLAUDE.md +1 -0
- databricks_agentbricks/templates/agent-openai/README.md +143 -0
- databricks_agentbricks/templates/agent-openai/agent/__init__.py +0 -0
- databricks_agentbricks/templates/agent-openai/agent/agent.py +205 -0
- databricks_agentbricks/templates/agent-openai/agent/mcps.py +28 -0
- databricks_agentbricks/templates/agent-openai/agent/tools/__init__.py +24 -0
- databricks_agentbricks/templates/agent-openai/agent/tools/sample_tool.py +15 -0
- databricks_agentbricks/templates/agent-openai/agent/tools/send_message.py +18 -0
- databricks_agentbricks/templates/agent-openai/app.yaml +15 -0
- databricks_agentbricks/templates/agent-openai/pyproject.toml +42 -0
- databricks_agentbricks/templates/agent-openai/runtime/__init__.py +0 -0
- databricks_agentbricks/templates/agent-openai/runtime/adapter.py +188 -0
- databricks_agentbricks/templates/agent-openai/runtime/main.py +25 -0
- databricks_agentbricks/templates/agent-openai/tests/__init__.py +0 -0
- databricks_agentbricks/templates/agent-openai/tests/test_agent.py +326 -0
- databricks_agentbricks/templates/agent-openai/tests/test_runtime.py +19 -0
- databricks_agentbricks/templates/agent-openai/tests/test_workspace.py +28 -0
- databricks_agentbricks/templates/custom-agent-langgraph/.env.example +1 -0
- databricks_agentbricks/templates/custom-agent-langgraph/.gitignore +11 -0
- databricks_agentbricks/templates/custom-agent-langgraph/README.md +22 -0
- databricks_agentbricks/templates/custom-agent-langgraph/agent/__init__.py +0 -0
- databricks_agentbricks/templates/custom-agent-langgraph/agent/agent.py +27 -0
- databricks_agentbricks/templates/custom-agent-langgraph/app.yaml +1 -0
- databricks_agentbricks/templates/custom-agent-langgraph/pyproject.toml +38 -0
- databricks_agentbricks/templates/custom-agent-langgraph/runtime/__init__.py +0 -0
- databricks_agentbricks/templates/custom-agent-langgraph/runtime/main.py +31 -0
- databricks_agentbricks/templates/custom-agent-langgraph/tests/test_runtime.py +29 -0
- databricks_agentbricks/templates/custom-agent-openai/.env.example +1 -0
- databricks_agentbricks/templates/custom-agent-openai/.gitignore +11 -0
- databricks_agentbricks/templates/custom-agent-openai/README.md +22 -0
- databricks_agentbricks/templates/custom-agent-openai/agent/__init__.py +0 -0
- databricks_agentbricks/templates/custom-agent-openai/agent/agent.py +29 -0
- databricks_agentbricks/templates/custom-agent-openai/app.yaml +1 -0
- databricks_agentbricks/templates/custom-agent-openai/pyproject.toml +38 -0
- databricks_agentbricks/templates/custom-agent-openai/runtime/__init__.py +0 -0
- databricks_agentbricks/templates/custom-agent-openai/runtime/main.py +31 -0
- databricks_agentbricks/templates/custom-agent-openai/tests/test_runtime.py +29 -0
- databricks_agentbricks/templates/sandbox_mcp.py +39 -0
- databricks_agentbricks/templates/sandbox_mcp_langgraph.py +39 -0
- databricks_agentbricks/templates/ui/agent-langgraph/CHAT_APP.md +49 -0
- databricks_agentbricks/templates/ui/agent-langgraph/runtime/main.py +26 -0
- databricks_agentbricks/templates/ui/agent-langgraph/runtime/ui.py +528 -0
- databricks_agentbricks/templates/ui/agent-langgraph/tests/test_demo_ui.py +542 -0
- databricks_agentbricks/templates/ui/agent-langgraph/ui/agentbricks-mark.png +0 -0
- databricks_agentbricks/templates/ui/agent-langgraph/ui/app.js +1464 -0
- databricks_agentbricks/templates/ui/agent-langgraph/ui/index.html +281 -0
- databricks_agentbricks/templates/ui/agent-langgraph/ui/styles.css +1913 -0
- databricks_agentbricks/templates/ui/agent-openai/CHAT_APP.md +54 -0
- databricks_agentbricks/templates/ui/agent-openai/runtime/main.py +26 -0
- databricks_agentbricks/templates/ui/agent-openai/runtime/ui.py +521 -0
- databricks_agentbricks/templates/ui/agent-openai/tests/test_demo_ui.py +512 -0
- databricks_agentbricks/templates/ui/agent-openai/ui/agentbricks-mark.png +0 -0
- databricks_agentbricks/templates/ui/agent-openai/ui/app.js +1428 -0
- databricks_agentbricks/templates/ui/agent-openai/ui/index.html +281 -0
- databricks_agentbricks/templates/ui/agent-openai/ui/styles.css +1913 -0
- databricks_agentbricks/theme.py +72 -0
- databricks_agentbricks-0.2.0.dist-info/METADATA +897 -0
- databricks_agentbricks-0.2.0.dist-info/RECORD +159 -0
- databricks_agentbricks-0.2.0.dist-info/WHEEL +4 -0
- databricks_agentbricks-0.2.0.dist-info/entry_points.txt +2 -0
- databricks_agentbricks-0.2.0.dist-info/licenses/NOTICE +39 -0
- databricks_agentkit/__init__.py +67 -0
- databricks_agentkit/_api_client.py +673 -0
- databricks_agentkit/_pagination.py +25 -0
- databricks_agentkit/client.py +31 -0
- databricks_agentkit/langgraph/__init__.py +98 -0
- databricks_agentkit/langgraph/genie.py +50 -0
- databricks_agentkit/langgraph/mcp.py +216 -0
- databricks_agentkit/langgraph/memory.py +67 -0
- databricks_agentkit/langgraph/session_store.py +485 -0
- databricks_agentkit/memory_store.py +371 -0
- databricks_agentkit/models.py +330 -0
- databricks_agentkit/openai/__init__.py +93 -0
- databricks_agentkit/openai/genie.py +50 -0
- databricks_agentkit/openai/mcp.py +189 -0
- databricks_agentkit/openai/memory.py +67 -0
- databricks_agentkit/openai/sessions.py +157 -0
- databricks_agentkit/py.typed +0 -0
- databricks_agentkit/runtime/ARCHITECTURE.md +64 -0
- databricks_agentkit/runtime/README.md +184 -0
- databricks_agentkit/runtime/__init__.py +69 -0
- databricks_agentkit/runtime/app.py +316 -0
- databricks_agentkit/runtime/auth.py +149 -0
- databricks_agentkit/runtime/durability/__init__.py +1 -0
- databricks_agentkit/runtime/durability/execution.py +133 -0
- databricks_agentkit/runtime/durability/heartbeat.py +64 -0
- databricks_agentkit/runtime/durability/lakebase_runtime_store.py +648 -0
- databricks_agentkit/runtime/durability/recovery.py +70 -0
- databricks_agentkit/runtime/durability/store.py +33 -0
- databricks_agentkit/runtime/execution.py +160 -0
- databricks_agentkit/runtime/genie.py +224 -0
- databricks_agentkit/runtime/mcp_auth.py +114 -0
- databricks_agentkit/runtime/model_services.py +90 -0
- databricks_agentkit/runtime/runtime.py +273 -0
- databricks_agentkit/runtime/session_store_client.py +120 -0
- databricks_agentkit/runtime/store.py +287 -0
- databricks_agentkit/runtime/tool_manifest.py +223 -0
- databricks_agentkit/runtime/tracing.py +71 -0
- databricks_agentkit/runtime/types.py +118 -0
- databricks_agentkit/runtime/workspace.py +27 -0
- databricks_agentkit/session_store.py +434 -0
- databricks_agentkit/timefmt.py +85 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""Lakebase/Apps resource plumbing for a deployed app.
|
|
2
|
+
|
|
3
|
+
Binds Databricks Apps resources onto an app so its service principal gets platform-managed grants:
|
|
4
|
+
a `postgres` resource for the legacy per-app Runtime Store and the tracing `experiment` resource.
|
|
5
|
+
The service-managed Runtime Store path grants database access through Conversation Store instead.
|
|
6
|
+
|
|
7
|
+
Managed-store (session/memory) table access is NOT granted here. The deployed app reaches those
|
|
8
|
+
stores over the conversation-store REST API, which grants the app's service principal read/write
|
|
9
|
+
server-side (see `deploy._grant_store_access`), so no direct Lakebase grant is needed.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
import subprocess
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
from typing import Optional
|
|
18
|
+
|
|
19
|
+
from databricks_agentbricks.databricks_cli import _databricks
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(frozen=True)
|
|
23
|
+
class LakebaseBackend:
|
|
24
|
+
"""A Lakebase database and endpoint bound as a `postgres` app resource."""
|
|
25
|
+
|
|
26
|
+
project: str
|
|
27
|
+
branch: str
|
|
28
|
+
endpoint_id: str
|
|
29
|
+
database: str
|
|
30
|
+
schema: str
|
|
31
|
+
tables: tuple[str, ...]
|
|
32
|
+
resource_name: str # the app-resource name (must be unique across an app's resources)
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def branch_path(self) -> str:
|
|
36
|
+
return f"projects/{self.project}/branches/{self.branch}"
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def database_path(self) -> str:
|
|
40
|
+
return f"{self.branch_path}/databases/{self.database}"
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def endpoint_path(self) -> str:
|
|
44
|
+
return f"{self.branch_path}/endpoints/{self.endpoint_id}"
|
|
45
|
+
|
|
46
|
+
def postgres_resource(self) -> dict:
|
|
47
|
+
"""The `postgres` app-resource entry that grants the SP a Lakebase role + CONNECT."""
|
|
48
|
+
return {
|
|
49
|
+
"name": self.resource_name,
|
|
50
|
+
"postgres": {
|
|
51
|
+
"branch": self.branch_path,
|
|
52
|
+
"database": self.database_path,
|
|
53
|
+
"permission": "CAN_CONNECT_AND_CREATE",
|
|
54
|
+
},
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _current_app_resources(app: str, profile: Optional[str]) -> list[dict]:
|
|
59
|
+
"""Read the app's existing resources array (empty list if it can't be read)."""
|
|
60
|
+
result = _databricks(["apps", "get", app, "-o", "json"], profile, capture=True, check=False)
|
|
61
|
+
if result.returncode != 0:
|
|
62
|
+
return []
|
|
63
|
+
try:
|
|
64
|
+
resources = json.loads(result.stdout or "{}").get("resources", [])
|
|
65
|
+
except (json.JSONDecodeError, AttributeError):
|
|
66
|
+
return []
|
|
67
|
+
return resources if isinstance(resources, list) else []
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# The app-resource name for the trace experiment (unique across an app's resources, like a store's).
|
|
71
|
+
_TRACE_EXPERIMENT_RESOURCE = "agentbricks-trace-experiment"
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def apply_experiment_resource(
|
|
75
|
+
app: str, experiment_id: str, profile: Optional[str]
|
|
76
|
+
) -> Optional[str]:
|
|
77
|
+
"""Bind the trace experiment as an `experiment` app resource so the SP can write traces.
|
|
78
|
+
|
|
79
|
+
This is the platform-managed grant: declaring the experiment as a `CAN_EDIT` resource lets the
|
|
80
|
+
app's service principal log traces to it (no manual SQL grant). Uses the same masked
|
|
81
|
+
read-modify-write as `apply_postgres_resources`: replace the whole resource array (preserving
|
|
82
|
+
every resource we don't own) while `update_mask` keeps the write from touching any other app
|
|
83
|
+
field. Returns None on success or a human-readable reason on failure.
|
|
84
|
+
"""
|
|
85
|
+
ours = {
|
|
86
|
+
"name": _TRACE_EXPERIMENT_RESOURCE,
|
|
87
|
+
"experiment": {"experiment_id": experiment_id, "permission": "CAN_EDIT"},
|
|
88
|
+
}
|
|
89
|
+
preserved = [
|
|
90
|
+
r
|
|
91
|
+
for r in _current_app_resources(app, profile)
|
|
92
|
+
if isinstance(r, dict) and r.get("name") != _TRACE_EXPERIMENT_RESOURCE
|
|
93
|
+
]
|
|
94
|
+
result = _update_app_resources(app, preserved + [ours], profile)
|
|
95
|
+
if result.returncode == 0:
|
|
96
|
+
return None
|
|
97
|
+
return (result.stderr or result.stdout or "").strip() or "unknown error"
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def apply_postgres_resources(
|
|
101
|
+
app: str, backends: list[LakebaseBackend], profile: Optional[str]
|
|
102
|
+
) -> Optional[str]:
|
|
103
|
+
"""Bind each backend's database as a `postgres` app resource in one update.
|
|
104
|
+
|
|
105
|
+
Writing the resources field REPLACES the whole array, so we must send the complete set:
|
|
106
|
+
read the app's current resources, drop the ones we manage (matched by name) so re-deploys
|
|
107
|
+
update rather than duplicate them, keep every other (user-owned) resource, and append ours.
|
|
108
|
+
Returns None on success or a human-readable reason on failure.
|
|
109
|
+
"""
|
|
110
|
+
ours = [b.postgres_resource() for b in backends]
|
|
111
|
+
our_names = {r["name"] for r in ours}
|
|
112
|
+
preserved = [
|
|
113
|
+
r
|
|
114
|
+
for r in _current_app_resources(app, profile)
|
|
115
|
+
if isinstance(r, dict) and r.get("name") not in our_names
|
|
116
|
+
]
|
|
117
|
+
result = _update_app_resources(app, preserved + ours, profile)
|
|
118
|
+
if result.returncode == 0:
|
|
119
|
+
return None
|
|
120
|
+
return (result.stderr or result.stdout or "").strip() or "unknown error"
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _update_app_resources(
|
|
124
|
+
app: str, resources: list[dict], profile: Optional[str]
|
|
125
|
+
) -> subprocess.CompletedProcess:
|
|
126
|
+
"""Set the app's resources array without disturbing any other app field.
|
|
127
|
+
|
|
128
|
+
`update_mask` scopes the write to `resources` only. A bare `apps update` replaces the whole app
|
|
129
|
+
spec, so fields we don't send — notably `user_api_scopes` — would reset to their defaults every
|
|
130
|
+
deploy, silently breaking OBO. The masked `create-update` still replaces the resources array
|
|
131
|
+
(callers pass the complete desired set), but leaves everything else untouched.
|
|
132
|
+
"""
|
|
133
|
+
payload = {"app": {"resources": resources}, "update_mask": "resources"}
|
|
134
|
+
return _databricks(
|
|
135
|
+
["apps", "create-update", app, "--json", json.dumps(payload)],
|
|
136
|
+
profile,
|
|
137
|
+
capture=True,
|
|
138
|
+
check=False,
|
|
139
|
+
)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Agent Bricks CLI: the ``ab`` command and its subcommands.
|
|
2
|
+
|
|
3
|
+
The console entrypoint (``project.scripts``) is ``databricks_agentbricks.cli:main``, re-exported here from
|
|
4
|
+
``databricks_agentbricks.cli.app`` (the command tree) so that path stays stable. The entry function is named
|
|
5
|
+
``app`` there rather than ``main`` so it does not shadow this package's re-exported ``main``. These
|
|
6
|
+
modules are the command surface; the SDK and deployed-agent runtime live under
|
|
7
|
+
``databricks_agentkit``.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from databricks_agentbricks.cli.app import main
|
|
11
|
+
|
|
12
|
+
__all__ = ["main"]
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""`ab` — the Databricks CLI for agent deployment, memory, and sessions.
|
|
2
|
+
|
|
3
|
+
Root Click group. Global `--profile` and `--output` flow to every subcommand via
|
|
4
|
+
`CliContext` on `ctx.obj`; subcommands build an authenticated API client on demand.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Optional
|
|
10
|
+
|
|
11
|
+
import click
|
|
12
|
+
|
|
13
|
+
from databricks_agentbricks import errors
|
|
14
|
+
from databricks_agentbricks.cli.auth import load_default_profile, login, logout
|
|
15
|
+
from databricks_agentbricks.cli.deploy import deploy, deployments
|
|
16
|
+
from databricks_agentbricks.cli.dev import dev
|
|
17
|
+
from databricks_agentbricks.cli.endpoint import endpoint
|
|
18
|
+
from databricks_agentbricks.cli.help import configure_help
|
|
19
|
+
from databricks_agentbricks.cli.init import init
|
|
20
|
+
from databricks_agentbricks.cli.memory import memory
|
|
21
|
+
from databricks_agentbricks.cli.sessions import sessions
|
|
22
|
+
from databricks_agentbricks.cli.tools import tools
|
|
23
|
+
from databricks_agentbricks.cli.tracing import tracing
|
|
24
|
+
from databricks_agentkit._api_client import _AgentBricksApiClient
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class CliContext:
|
|
28
|
+
"""Shared per-invocation state: selected profile, output mode, lazily-built client."""
|
|
29
|
+
|
|
30
|
+
def __init__(self, profile: Optional[str], output: str):
|
|
31
|
+
self.profile = profile
|
|
32
|
+
self.output = output
|
|
33
|
+
self._client: Optional[_AgentBricksApiClient] = None
|
|
34
|
+
|
|
35
|
+
def client(self) -> _AgentBricksApiClient:
|
|
36
|
+
if self._client is None:
|
|
37
|
+
self._client = _AgentBricksApiClient(self.profile)
|
|
38
|
+
return self._client
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@click.group(name="ab", context_settings={"help_option_names": ["-h", "--help"]})
|
|
42
|
+
@click.option(
|
|
43
|
+
"--profile", "-p", default=None, help="~/.databrickscfg profile to authenticate with."
|
|
44
|
+
)
|
|
45
|
+
@click.option(
|
|
46
|
+
"--output",
|
|
47
|
+
"-o",
|
|
48
|
+
type=click.Choice(["text", "json"]),
|
|
49
|
+
default="text",
|
|
50
|
+
help="Output format (default: text).",
|
|
51
|
+
)
|
|
52
|
+
@click.version_option(package_name="databricks-agentbricks")
|
|
53
|
+
@click.pass_context
|
|
54
|
+
def agentbricks(ctx: click.Context, profile: Optional[str], output: str) -> None:
|
|
55
|
+
"""Agent Bricks is a CLI for building and deploying custom AI agents on Databricks.
|
|
56
|
+
|
|
57
|
+
The Agent Bricks CLI is experimental: its commands and the underlying agent APIs are all in
|
|
58
|
+
preview, may need to be enabled for your workspace, and are likely to change in
|
|
59
|
+
backward-incompatible ways.
|
|
60
|
+
|
|
61
|
+
Scaffold an agent project from a template, run it locally with a chat UI, and deploy it to
|
|
62
|
+
Databricks Apps — then manage the tools, memory, sessions, and tracing behind it, all from one
|
|
63
|
+
authenticated command.
|
|
64
|
+
|
|
65
|
+
New here? The examples below take you from an empty directory to a deployed agent. Agent Bricks
|
|
66
|
+
authenticates with a Databricks profile: run `ab login` once to save a default, or pass
|
|
67
|
+
--profile / -p (without one, the Databricks SDK's default authentication is used).
|
|
68
|
+
|
|
69
|
+
Agents built with Agent Bricks combine the platform's capabilities:
|
|
70
|
+
|
|
71
|
+
\b
|
|
72
|
+
Models Call Databricks model serving out of the box, routed through
|
|
73
|
+
the AI Gateway for capacity on your existing Databricks auth.
|
|
74
|
+
Tools Data sandboxes, managed MCP services, Unity Catalog
|
|
75
|
+
functions, and local Python tools the agent can call.
|
|
76
|
+
Memory Long-term memory the agent recalls across conversations.
|
|
77
|
+
Sessions The transcript, history, and state of a single conversation.
|
|
78
|
+
Tracing MLflow traces in Unity Catalog to debug and evaluate runs.
|
|
79
|
+
Deployment Hosting on Databricks Apps, with scaling and sticky routing.
|
|
80
|
+
|
|
81
|
+
`ab deploy` provisions and wires these into a single agent hosted on Databricks Apps.
|
|
82
|
+
"""
|
|
83
|
+
# Let errors render to match the selected output mode (JSON errors for -o json).
|
|
84
|
+
errors.set_output_mode(output)
|
|
85
|
+
ctx.obj = CliContext(profile=profile or load_default_profile(), output=output)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
agentbricks.add_command(login)
|
|
89
|
+
agentbricks.add_command(logout)
|
|
90
|
+
agentbricks.add_command(init)
|
|
91
|
+
agentbricks.add_command(dev)
|
|
92
|
+
agentbricks.add_command(memory)
|
|
93
|
+
agentbricks.add_command(sessions)
|
|
94
|
+
agentbricks.add_command(tracing)
|
|
95
|
+
agentbricks.add_command(deploy)
|
|
96
|
+
agentbricks.add_command(deployments)
|
|
97
|
+
agentbricks.add_command(endpoint)
|
|
98
|
+
agentbricks.add_command(tools)
|
|
99
|
+
configure_help(agentbricks)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def main() -> None:
|
|
103
|
+
# Click derives the display name from argv[0] so the `ab` script is shown in help output.
|
|
104
|
+
agentbricks()
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
if __name__ == "__main__":
|
|
108
|
+
main()
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"""Preflight and conservative Apps scope reconciliation for request-user tools."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import time
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from datetime import timedelta
|
|
8
|
+
|
|
9
|
+
from databricks.sdk import WorkspaceClient
|
|
10
|
+
from databricks.sdk.errors import DatabricksError, NotFound
|
|
11
|
+
from databricks.sdk.service.apps import App, AppsAPI
|
|
12
|
+
|
|
13
|
+
from databricks_agentbricks.agent_project import AgentProject
|
|
14
|
+
from databricks_agentbricks.errors import AgentCliError
|
|
15
|
+
from databricks_agentbricks.project_types import AgentServer
|
|
16
|
+
|
|
17
|
+
_IDENTITY_DEFAULT_SCOPES = frozenset({"iam.access-control:read", "iam.current-user:read"})
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _validate_forwarding(app: App) -> None:
|
|
21
|
+
if app.forward_user_access_token is False:
|
|
22
|
+
raise AgentCliError(
|
|
23
|
+
f"App '{app.name}' has forward_user_access_token=False; user auth requires forwarding.",
|
|
24
|
+
hint="Enable user access token forwarding in Databricks Apps, then stop and start "
|
|
25
|
+
"the App compute for the setting to take effect before retrying deployment.",
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _validate_implicit_identity_scopes(app: App) -> None:
|
|
30
|
+
if app.user_api_scopes is not None:
|
|
31
|
+
return
|
|
32
|
+
effective = set(app.effective_user_api_scopes or [])
|
|
33
|
+
if effective - _IDENTITY_DEFAULT_SCOPES:
|
|
34
|
+
raise AgentCliError(
|
|
35
|
+
"Apps did not return configured scopes for an App with unexplained effective grants.",
|
|
36
|
+
hint="Inspect the App's scope configuration with its owner before retrying the scope "
|
|
37
|
+
"update.",
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def requires_user_auth(project: AgentProject | None) -> bool:
|
|
42
|
+
"""Infer request-user auth from tool bindings before any deployment mutation."""
|
|
43
|
+
if project is None or not project.tools:
|
|
44
|
+
return False
|
|
45
|
+
managed = [
|
|
46
|
+
tool
|
|
47
|
+
for tool in project.tools
|
|
48
|
+
if tool.source.kind in ("mcp", "sandbox", "genie_one", "genie_agent")
|
|
49
|
+
]
|
|
50
|
+
user_auth = any(tool.auth == "user" for tool in managed)
|
|
51
|
+
if user_auth and project.server != AgentServer.AGENTBRICKS:
|
|
52
|
+
raise AgentCliError(
|
|
53
|
+
"Managed tools with auth = 'user' require [agent].server = 'agentbricks'.",
|
|
54
|
+
hint="Migrate to the request-auth-aware Agent Bricks DurableAgentServer template before enabling user "
|
|
55
|
+
"auth. Failure recovery is unsupported for request-user attempts because the credential "
|
|
56
|
+
"is transient.",
|
|
57
|
+
)
|
|
58
|
+
if user_auth:
|
|
59
|
+
unspecified = [tool.id for tool in managed if tool.auth is None]
|
|
60
|
+
if unspecified:
|
|
61
|
+
raise AgentCliError(
|
|
62
|
+
"Request-user tools require explicit auth on every managed "
|
|
63
|
+
f"tool binding: {', '.join(unspecified)}.",
|
|
64
|
+
hint="Choose auth = 'app' to preserve legacy identity, or explicitly choose 'user'.",
|
|
65
|
+
)
|
|
66
|
+
return user_auth
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass(frozen=True)
|
|
70
|
+
class AppUserScopeUpdatePlan:
|
|
71
|
+
apps: AppsAPI
|
|
72
|
+
name: str
|
|
73
|
+
existing_scopes: tuple[str, ...] | None
|
|
74
|
+
scopes: tuple[str, ...]
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def required_user_api_scopes(project: AgentProject | None) -> set[str]:
|
|
78
|
+
"""Return Databricks Apps user API scopes required by request-user tools."""
|
|
79
|
+
scopes: set[str] = set()
|
|
80
|
+
# TODO: Extend this least-privilege mapping for each supported request-user tool kind/service.
|
|
81
|
+
for tool in project.tools if project else ():
|
|
82
|
+
if tool.auth != "user":
|
|
83
|
+
continue
|
|
84
|
+
if tool.source.kind in ("genie_one", "genie_agent"):
|
|
85
|
+
scopes.add("genie")
|
|
86
|
+
continue
|
|
87
|
+
if tool.source.kind not in ("mcp", "sandbox"):
|
|
88
|
+
continue
|
|
89
|
+
scopes.add("ai-gateway")
|
|
90
|
+
if tool.source.service == "system.ai.dbsql":
|
|
91
|
+
scopes.add("sql")
|
|
92
|
+
if tool.source.service == "system.ai.genie_one_mcp":
|
|
93
|
+
scopes.add("genie")
|
|
94
|
+
if tool.source.kind == "sandbox" and any(
|
|
95
|
+
scope.kind == "volume" for scope in tool.policy.downscope
|
|
96
|
+
):
|
|
97
|
+
scopes.add("files")
|
|
98
|
+
return scopes
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def plan_app_user_scope_update(
|
|
102
|
+
name: str,
|
|
103
|
+
profile: str | None,
|
|
104
|
+
*,
|
|
105
|
+
allow_existing_app_update: bool,
|
|
106
|
+
required_scopes: set[str] | None = None,
|
|
107
|
+
) -> AppUserScopeUpdatePlan:
|
|
108
|
+
"""Plan user-scope creation or addition without changing the Databricks App.
|
|
109
|
+
|
|
110
|
+
A new App can be created with the requested user API scopes automatically. An existing App is
|
|
111
|
+
left unchanged unless all requested scopes are already present or the caller explicitly allows
|
|
112
|
+
Agent Bricks to add the missing scopes.
|
|
113
|
+
"""
|
|
114
|
+
try:
|
|
115
|
+
apps = WorkspaceClient(profile=profile).apps
|
|
116
|
+
try:
|
|
117
|
+
existing = apps.get(name)
|
|
118
|
+
except NotFound:
|
|
119
|
+
existing = None
|
|
120
|
+
except (DatabricksError, ValueError) as exc:
|
|
121
|
+
raise AgentCliError(f"Could not read Apps user scopes for '{name}'.") from exc
|
|
122
|
+
if existing is not None:
|
|
123
|
+
_validate_forwarding(existing)
|
|
124
|
+
if existing is not None:
|
|
125
|
+
_validate_implicit_identity_scopes(existing)
|
|
126
|
+
configured = tuple(sorted(set(existing.user_api_scopes or []))) if existing else None
|
|
127
|
+
requested = {"ai-gateway"} if required_scopes is None else required_scopes
|
|
128
|
+
scopes = tuple(sorted({*(configured or ()), *requested}))
|
|
129
|
+
if existing is not None and scopes != configured and not allow_existing_app_update:
|
|
130
|
+
raise AgentCliError(
|
|
131
|
+
f"App '{name}' is missing required user API scopes; re-run with "
|
|
132
|
+
"--allow-user-scope-update to add them.",
|
|
133
|
+
hint="Review the existing App scopes and coordinate with other owners first. Agent Bricks "
|
|
134
|
+
"preserves unrelated scopes; later deploys do not need the flag once all required "
|
|
135
|
+
"scopes are present.",
|
|
136
|
+
)
|
|
137
|
+
return AppUserScopeUpdatePlan(apps=apps, name=name, existing_scopes=configured, scopes=scopes)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def apply_app_user_scope_update(
|
|
141
|
+
plan: AppUserScopeUpdatePlan, *, instances: int | None = None, attempts: int = 12
|
|
142
|
+
) -> None:
|
|
143
|
+
"""Create a scoped App or add missing user API scopes to an existing App.
|
|
144
|
+
|
|
145
|
+
New Apps enable user-token forwarding at creation. Existing Apps retain unrelated scopes and
|
|
146
|
+
settings; the update mask includes only user API scopes and explicitly requested instance
|
|
147
|
+
fields. The read-before-write check detects known drift, but Apps has no compare-and-swap
|
|
148
|
+
contract, so owners must still coordinate concurrent updates. Scope removal is never automatic.
|
|
149
|
+
"""
|
|
150
|
+
if not plan.scopes or not set(plan.existing_scopes or ()).issubset(plan.scopes):
|
|
151
|
+
raise AgentCliError(
|
|
152
|
+
"Automatic removal of Apps user scopes is unsupported.",
|
|
153
|
+
hint="Remove scopes explicitly in Databricks Apps and verify the effective scopes. "
|
|
154
|
+
"The SDK omits empty lists when serializing App; no removal was sent.",
|
|
155
|
+
)
|
|
156
|
+
desired = App(
|
|
157
|
+
name=plan.name,
|
|
158
|
+
user_api_scopes=list(plan.scopes),
|
|
159
|
+
forward_user_access_token=True if plan.existing_scopes is None else None,
|
|
160
|
+
)
|
|
161
|
+
mask = ["user_api_scopes"]
|
|
162
|
+
if instances is not None:
|
|
163
|
+
desired.compute_min_instances = instances
|
|
164
|
+
desired.compute_max_instances = instances
|
|
165
|
+
mask.extend(["compute_min_instances", "compute_max_instances"])
|
|
166
|
+
try:
|
|
167
|
+
if plan.existing_scopes is None:
|
|
168
|
+
plan.apps.create(desired)
|
|
169
|
+
else:
|
|
170
|
+
current = plan.apps.get(plan.name)
|
|
171
|
+
_validate_forwarding(current)
|
|
172
|
+
_validate_implicit_identity_scopes(current)
|
|
173
|
+
if tuple(sorted(set(current.user_api_scopes or []))) != plan.existing_scopes:
|
|
174
|
+
raise AgentCliError("Apps user scopes changed since preflight; review and retry.")
|
|
175
|
+
if plan.scopes != plan.existing_scopes or instances is not None:
|
|
176
|
+
plan.apps.create_update(plan.name, update_mask=",".join(mask), app=desired).result(
|
|
177
|
+
timeout=timedelta(minutes=5)
|
|
178
|
+
)
|
|
179
|
+
for attempt in range(attempts):
|
|
180
|
+
current = plan.apps.get(plan.name)
|
|
181
|
+
_validate_forwarding(current)
|
|
182
|
+
desired_scopes = set(plan.scopes)
|
|
183
|
+
effective = set(current.effective_user_api_scopes or [])
|
|
184
|
+
if (
|
|
185
|
+
set(current.user_api_scopes or []) == desired_scopes
|
|
186
|
+
and desired_scopes.issubset(effective)
|
|
187
|
+
and effective.issubset(desired_scopes | _IDENTITY_DEFAULT_SCOPES)
|
|
188
|
+
):
|
|
189
|
+
return
|
|
190
|
+
if attempt + 1 < attempts:
|
|
191
|
+
time.sleep(5)
|
|
192
|
+
except (DatabricksError, TimeoutError) as exc:
|
|
193
|
+
raise AgentCliError(f"Could not reconcile Apps user scopes for '{plan.name}'.") from exc
|
|
194
|
+
raise AgentCliError(
|
|
195
|
+
f"App '{plan.name}' effective user scopes did not converge after {attempts} checks.",
|
|
196
|
+
hint="Source deployment was stopped. Inspect requested/effective scopes in Databricks "
|
|
197
|
+
"Apps; contact your platform administrator if propagation remains blocked. Then retry.",
|
|
198
|
+
)
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""`ab 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
|
+
Agent Bricks' saved selection, not the underlying credentials. State lives in a small JSON file under
|
|
8
|
+
`~/.agentbricks` (override the directory with `AGENTBRICKS_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
|
+
|
|
22
|
+
from databricks_agentbricks import render
|
|
23
|
+
from databricks_agentbricks.errors import AgentCliError
|
|
24
|
+
from databricks_agentkit._api_client import _AgentBricksApiClient
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _config_file() -> pathlib.Path:
|
|
28
|
+
base = os.environ.get("AGENTBRICKS_CONFIG_HOME")
|
|
29
|
+
root = pathlib.Path(base) if base else pathlib.Path.home() / ".agentbricks"
|
|
30
|
+
return root / "config.json"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def load_default_profile() -> Optional[str]:
|
|
34
|
+
"""The profile saved by `ab login`, or None if the user never logged in."""
|
|
35
|
+
try:
|
|
36
|
+
return json.loads(_config_file().read_text()).get("profile")
|
|
37
|
+
except (OSError, json.JSONDecodeError):
|
|
38
|
+
return None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _save_default_profile(profile: str) -> None:
|
|
42
|
+
path = _config_file()
|
|
43
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
44
|
+
path.write_text(json.dumps({"profile": profile}, indent=2) + "\n")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _validate_profile(profile: str) -> tuple[_AgentBricksApiClient, str]:
|
|
48
|
+
client = _AgentBricksApiClient(profile)
|
|
49
|
+
return client, client.current_user
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _is_interactive() -> bool:
|
|
53
|
+
return sys.stdin.isatty()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _run_databricks_login(profile: str) -> None:
|
|
57
|
+
command = ["databricks", "auth", "login", "--profile", profile]
|
|
58
|
+
try:
|
|
59
|
+
# Keep the child process interactive while preserving stdout for Agent Bricks JSON output.
|
|
60
|
+
result = subprocess.run(command, text=True, check=False, stdout=sys.stderr)
|
|
61
|
+
except FileNotFoundError as exc:
|
|
62
|
+
raise AgentCliError(
|
|
63
|
+
"Could not configure Databricks authentication: the `databricks` CLI was not found.",
|
|
64
|
+
hint=f"Install the Databricks CLI, then retry `ab login --profile {profile}`.",
|
|
65
|
+
) from exc
|
|
66
|
+
if result.returncode != 0:
|
|
67
|
+
raise AgentCliError(
|
|
68
|
+
f"`databricks auth login --profile {profile}` failed (exit {result.returncode})."
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _authenticate_profile(profile: str) -> tuple[_AgentBricksApiClient, str]:
|
|
73
|
+
# Local import: pulling databricks.sdk.errors loads the full SDK (~0.7s), which we defer off
|
|
74
|
+
# the CLI startup path. This function already builds a client, so the cost lands here anyway.
|
|
75
|
+
from databricks.sdk.errors import Unauthenticated
|
|
76
|
+
|
|
77
|
+
try:
|
|
78
|
+
return _validate_profile(profile)
|
|
79
|
+
except (AgentCliError, Unauthenticated) as initial_error:
|
|
80
|
+
if not _is_interactive():
|
|
81
|
+
raise AgentCliError(
|
|
82
|
+
f"Could not validate Databricks profile {profile!r}: {initial_error}",
|
|
83
|
+
hint="Run this command in an interactive terminal so Agent Bricks can open "
|
|
84
|
+
"Databricks login, or authenticate first with "
|
|
85
|
+
f"`databricks auth login --profile {profile}`.",
|
|
86
|
+
) from initial_error
|
|
87
|
+
except Exception as validation_error: # noqa: BLE001 - normalize unexpected API failures
|
|
88
|
+
raise AgentCliError(
|
|
89
|
+
f"Could not validate Databricks profile {profile!r}: {validation_error}"
|
|
90
|
+
) from validation_error
|
|
91
|
+
|
|
92
|
+
_run_databricks_login(profile)
|
|
93
|
+
try:
|
|
94
|
+
return _validate_profile(profile)
|
|
95
|
+
except Exception as retry_error: # noqa: BLE001 - normalize the post-login failure
|
|
96
|
+
raise AgentCliError(
|
|
97
|
+
f"Databricks login completed, but profile {profile!r} could not be validated: "
|
|
98
|
+
f"{retry_error}"
|
|
99
|
+
) from retry_error
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@click.command()
|
|
103
|
+
@click.option(
|
|
104
|
+
"--profile",
|
|
105
|
+
"-p",
|
|
106
|
+
default=None,
|
|
107
|
+
help="Profile to authenticate with and remember as the default.",
|
|
108
|
+
)
|
|
109
|
+
@click.pass_obj
|
|
110
|
+
def login(obj, profile) -> None:
|
|
111
|
+
"""Authenticate a profile and save it as the default, so later commands can omit -p."""
|
|
112
|
+
profile = profile or obj.profile
|
|
113
|
+
if not profile:
|
|
114
|
+
raise AgentCliError(
|
|
115
|
+
"No profile to save.",
|
|
116
|
+
hint="Pass one to remember, e.g. `ab login --profile <profile>`.",
|
|
117
|
+
)
|
|
118
|
+
client, user = _authenticate_profile(profile)
|
|
119
|
+
_save_default_profile(profile)
|
|
120
|
+
if obj.output == "json":
|
|
121
|
+
render.emit_json({"profile": profile, "user": user, "host": client.host})
|
|
122
|
+
return
|
|
123
|
+
render.success(
|
|
124
|
+
f"Logged in as {user}",
|
|
125
|
+
fields={"Profile": profile, "Host": client.host},
|
|
126
|
+
next_steps=[
|
|
127
|
+
("ab init my-agent", "Scaffold a new agent project"),
|
|
128
|
+
],
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@click.command()
|
|
133
|
+
@click.pass_obj
|
|
134
|
+
def logout(obj) -> None:
|
|
135
|
+
"""Forget the saved profile selection without deleting its credentials."""
|
|
136
|
+
path = _config_file()
|
|
137
|
+
existed = path.exists()
|
|
138
|
+
path.unlink(missing_ok=True)
|
|
139
|
+
if obj.output == "json":
|
|
140
|
+
render.emit_json({"logged_out": existed})
|
|
141
|
+
return
|
|
142
|
+
render.success("Logged out" if existed else "No saved login to clear")
|