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,38 @@
|
|
|
1
|
+
"""Thin wrapper around the ``databricks`` CLI.
|
|
2
|
+
|
|
3
|
+
Agent Bricks shells out to the ``databricks`` CLI for the operations it doesn't call over the REST client
|
|
4
|
+
(``apps`` lifecycle, ``sync``, ``postgres`` endpoint/credential lookups). This is the single place
|
|
5
|
+
that builds the argv, threads the ``--profile`` through, and normalizes a non-zero exit into an
|
|
6
|
+
``AgentCliError``. Kept separate from any one caller so ``deploy``, ``dev``, and ``app_resources``
|
|
7
|
+
share it without importing each other.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import subprocess
|
|
13
|
+
from typing import Optional
|
|
14
|
+
|
|
15
|
+
from databricks_agentbricks.errors import AgentCliError
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _databricks(
|
|
19
|
+
args: list[str],
|
|
20
|
+
profile: Optional[str],
|
|
21
|
+
*,
|
|
22
|
+
capture: bool = False,
|
|
23
|
+
check: bool = True,
|
|
24
|
+
cwd: Optional[str] = None,
|
|
25
|
+
action: Optional[str] = None,
|
|
26
|
+
) -> subprocess.CompletedProcess:
|
|
27
|
+
cmd = ["databricks", *args]
|
|
28
|
+
if profile:
|
|
29
|
+
cmd += ["--profile", profile]
|
|
30
|
+
result = subprocess.run(cmd, text=True, capture_output=capture, cwd=cwd)
|
|
31
|
+
if check and result.returncode != 0:
|
|
32
|
+
# Agent Bricks drives the `databricks apps` CLI as an implementation detail; surface a failure in
|
|
33
|
+
# Agent Bricks' own terms (`action`) rather than echoing the raw subcommand and --profile, which
|
|
34
|
+
# leaks the underlying tool at the customer. The captured stderr still rides along as the
|
|
35
|
+
# hint so debugging isn't lost.
|
|
36
|
+
detail = (result.stderr or result.stdout or "").strip() if capture else None
|
|
37
|
+
raise AgentCliError(action or "A Databricks CLI command failed.", hint=detail)
|
|
38
|
+
return result
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""Error types for the Agent Bricks CLI and mapping from Databricks REST errors.
|
|
2
|
+
|
|
3
|
+
`AgentCliError` extends `click.ClickException` so a raised error prints as a clean
|
|
4
|
+
one-liner (plus an optional hint) and exits non-zero, instead of dumping a traceback.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
12
|
+
import click
|
|
13
|
+
from rich.console import Console
|
|
14
|
+
|
|
15
|
+
# Error codes indicating that a preview API is unavailable in the workspace.
|
|
16
|
+
_PREVIEW_ERROR_CODES = frozenset({"NOT_IMPLEMENTED", "UNIMPLEMENTED", "FEATURE_DISABLED"})
|
|
17
|
+
|
|
18
|
+
# gRPC-style status codes the backend returns for transient failures (a cancelled or
|
|
19
|
+
# deadline-exceeded RPC, a briefly unavailable service). Shared with the client, which
|
|
20
|
+
# retries these before surfacing them. `client._do` retries; the hint here covers the
|
|
21
|
+
# case where retries are still exhausted.
|
|
22
|
+
TRANSIENT_ERROR_CODES = frozenset({"CANCELLED", "UNAVAILABLE", "DEADLINE_EXCEEDED", "ABORTED"})
|
|
23
|
+
|
|
24
|
+
# Process-global output mode, set once by the root CLI group. When "json", errors are
|
|
25
|
+
# emitted as a machine-readable JSON object instead of the styled text one-liner, so a
|
|
26
|
+
# script driving `ab -o json` can parse failures instead of scraping human text.
|
|
27
|
+
_OUTPUT_MODE = "text"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def set_output_mode(mode: str) -> None:
|
|
31
|
+
"""Record the CLI's --output mode so errors can render to match it."""
|
|
32
|
+
global _OUTPUT_MODE
|
|
33
|
+
_OUTPUT_MODE = mode
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
_PREVIEW_HINT = (
|
|
37
|
+
"These 2.0 agents APIs are in preview and gated per workspace. This handler is "
|
|
38
|
+
"not enabled on the target workspace yet — try a different --profile or contact "
|
|
39
|
+
"your workspace administrator."
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
_TRANSIENT_HINT = (
|
|
43
|
+
"This is usually a transient backend issue — re-running the command often succeeds."
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class AgentCliError(click.ClickException):
|
|
48
|
+
"""A user-facing CLI error rendered without a Python traceback."""
|
|
49
|
+
|
|
50
|
+
def __init__(
|
|
51
|
+
self, message: str, *, error_code: Optional[str] = None, hint: Optional[str] = None
|
|
52
|
+
):
|
|
53
|
+
super().__init__(message)
|
|
54
|
+
self.error_code = error_code
|
|
55
|
+
self.hint = hint
|
|
56
|
+
|
|
57
|
+
def show(self, file=None) -> None:
|
|
58
|
+
if _OUTPUT_MODE == "json":
|
|
59
|
+
payload: dict = {"message": self.message}
|
|
60
|
+
if self.error_code:
|
|
61
|
+
payload["code"] = self.error_code
|
|
62
|
+
if self.hint:
|
|
63
|
+
payload["hint"] = self.hint
|
|
64
|
+
click.echo(json.dumps({"error": payload}, indent=2), err=True)
|
|
65
|
+
return
|
|
66
|
+
# Render in the cargo/uv diagnostic grammar: a red `error:` (or cargo's
|
|
67
|
+
# `error[CODE]:`) keyword, the message, and an indented `help:` line carrying the fix.
|
|
68
|
+
from databricks_agentbricks import render
|
|
69
|
+
|
|
70
|
+
render.diagnostic(
|
|
71
|
+
"error",
|
|
72
|
+
self.message,
|
|
73
|
+
code=self.error_code,
|
|
74
|
+
help=self.hint,
|
|
75
|
+
con=Console(stderr=True),
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def wrap_api_error(exc: Exception) -> AgentCliError:
|
|
80
|
+
"""Convert a databricks-sdk error (or any exception) into an `AgentCliError`.
|
|
81
|
+
|
|
82
|
+
The SDK raises `databricks.sdk.errors.DatabricksError` subclasses carrying an
|
|
83
|
+
`error_code` attribute; we stay duck-typed so we don't couple to the SDK's error
|
|
84
|
+
hierarchy or version.
|
|
85
|
+
"""
|
|
86
|
+
# When the SDK exhausts its retry budget it raises a TimeoutError chaining the last underlying
|
|
87
|
+
# error as __cause__; surface that original service error, not the generic retry wrapper.
|
|
88
|
+
# __cause__ is typed BaseException, so use a separate BaseException-typed local rather than
|
|
89
|
+
# reassigning the Exception-typed parameter.
|
|
90
|
+
effective: BaseException = exc
|
|
91
|
+
if isinstance(exc, TimeoutError) and exc.__cause__ is not None:
|
|
92
|
+
effective = exc.__cause__
|
|
93
|
+
error_code = getattr(effective, "error_code", None)
|
|
94
|
+
# A message-less DatabricksError wraps `IOError(None)`, so `str(effective)` is the literal
|
|
95
|
+
# "None". Treat that (and an empty string) as "no detail" so we surface the error code
|
|
96
|
+
# instead of a bare `Error [CANCELLED]: None`.
|
|
97
|
+
detail = str(effective).strip()
|
|
98
|
+
if detail == "None":
|
|
99
|
+
detail = ""
|
|
100
|
+
message = detail or _no_detail_message(error_code) or effective.__class__.__name__
|
|
101
|
+
if error_code in _PREVIEW_ERROR_CODES:
|
|
102
|
+
hint = _PREVIEW_HINT
|
|
103
|
+
elif error_code in TRANSIENT_ERROR_CODES:
|
|
104
|
+
hint = _TRANSIENT_HINT
|
|
105
|
+
else:
|
|
106
|
+
hint = None
|
|
107
|
+
return AgentCliError(message, error_code=error_code, hint=hint)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _no_detail_message(error_code: Optional[str]) -> Optional[str]:
|
|
111
|
+
"""Fallback message when the server returns an error code but no human-readable detail."""
|
|
112
|
+
if not error_code:
|
|
113
|
+
return None
|
|
114
|
+
return f"The server returned {error_code} with no further detail."
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""Resolve service-managed Runtime Store backends from the API response."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from databricks_agentbricks.errors import AgentCliError
|
|
9
|
+
from databricks_agentkit import models
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True)
|
|
13
|
+
class RuntimeStoreBackend:
|
|
14
|
+
"""Connection coordinates returned by the Runtime Store API."""
|
|
15
|
+
|
|
16
|
+
branch: str
|
|
17
|
+
database_id: str
|
|
18
|
+
username: str
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def get_or_create_backend(
|
|
22
|
+
client: Any, app: str, app_service_principal_id: str | None
|
|
23
|
+
) -> RuntimeStoreBackend:
|
|
24
|
+
"""Create or reuse the Runtime Store owned by a Databricks App."""
|
|
25
|
+
if not app_service_principal_id:
|
|
26
|
+
raise AgentCliError("Could not resolve the app's service principal for its Runtime Store.")
|
|
27
|
+
try:
|
|
28
|
+
store = client.create_runtime_store(
|
|
29
|
+
app,
|
|
30
|
+
app_service_principal_id,
|
|
31
|
+
app_name=app,
|
|
32
|
+
retry_transient=True,
|
|
33
|
+
)
|
|
34
|
+
except AgentCliError as exc:
|
|
35
|
+
if exc.error_code != "ALREADY_EXISTS":
|
|
36
|
+
raise
|
|
37
|
+
store = client.get_runtime_store(app)
|
|
38
|
+
return backend_from_api(app, app_service_principal_id, store)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def delete(client: Any, app: str, app_service_principal_id: str) -> None:
|
|
42
|
+
"""Delete the managed store while preserving the app when cleanup needs a retry."""
|
|
43
|
+
try:
|
|
44
|
+
store = client.get_runtime_store(app)
|
|
45
|
+
validate_owner(app, app_service_principal_id, store)
|
|
46
|
+
client.delete_runtime_store(app)
|
|
47
|
+
except AgentCliError as exc:
|
|
48
|
+
if exc.error_code == "NOT_FOUND":
|
|
49
|
+
return
|
|
50
|
+
raise AgentCliError(
|
|
51
|
+
f"Could not delete Runtime Store '{app}': {exc.message}",
|
|
52
|
+
error_code=exc.error_code,
|
|
53
|
+
hint=f"The deployment was retained. Retry `ab deployments delete {app}` "
|
|
54
|
+
"after resolving the Runtime Store error.",
|
|
55
|
+
) from exc
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _model(runtime_store: Any) -> models.RuntimeStore:
|
|
59
|
+
if not isinstance(runtime_store, dict):
|
|
60
|
+
raise AgentCliError("Runtime Store API returned an invalid resource.")
|
|
61
|
+
return models.RuntimeStore(runtime_store)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def validate_owner(
|
|
65
|
+
app: str, app_service_principal_id: str, runtime_store: Any
|
|
66
|
+
) -> models.RuntimeStore:
|
|
67
|
+
"""Refuse to reuse or delete a Runtime Store owned by another app identity."""
|
|
68
|
+
store = _model(runtime_store)
|
|
69
|
+
if store.name != f"runtime-stores/{app}":
|
|
70
|
+
raise AgentCliError("Runtime Store API returned an unexpected resource name.")
|
|
71
|
+
owner = store.owner
|
|
72
|
+
app_owner = owner.app if owner is not None else None
|
|
73
|
+
if (
|
|
74
|
+
app_owner is None
|
|
75
|
+
or app_owner.name != app
|
|
76
|
+
or app_owner.service_principal_id != app_service_principal_id
|
|
77
|
+
):
|
|
78
|
+
raise AgentCliError(f"Runtime Store '{app}' does not belong to this app identity.")
|
|
79
|
+
return store
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def backend_from_api(
|
|
83
|
+
app: str, app_service_principal_id: str, runtime_store: Any
|
|
84
|
+
) -> RuntimeStoreBackend:
|
|
85
|
+
"""Return only the connection coordinates supplied by the Runtime Store API."""
|
|
86
|
+
store = validate_owner(app, app_service_principal_id, runtime_store)
|
|
87
|
+
storage_backend = store.storage_backend
|
|
88
|
+
lakebase = storage_backend.lakebase if storage_backend is not None else None
|
|
89
|
+
branch = lakebase.branch if lakebase is not None else None
|
|
90
|
+
database_id = lakebase.database_id if lakebase is not None else None
|
|
91
|
+
owner = store.owner
|
|
92
|
+
app_owner = owner.app if owner is not None else None
|
|
93
|
+
username = app_owner.service_principal_id if app_owner is not None else None
|
|
94
|
+
if (
|
|
95
|
+
not isinstance(branch, str)
|
|
96
|
+
or not branch
|
|
97
|
+
or not isinstance(database_id, str)
|
|
98
|
+
or not database_id
|
|
99
|
+
or not isinstance(username, str)
|
|
100
|
+
or not username
|
|
101
|
+
):
|
|
102
|
+
raise AgentCliError("Runtime Store API returned an incomplete Lakebase backend.")
|
|
103
|
+
return RuntimeStoreBackend(
|
|
104
|
+
branch=branch,
|
|
105
|
+
database_id=database_id,
|
|
106
|
+
username=username,
|
|
107
|
+
)
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Legacy per-app Lakebase project provisioning for Agent Bricks Runtime Store."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
import re
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
from databricks_agentbricks.app_resources import LakebaseBackend
|
|
11
|
+
from databricks_agentbricks.databricks_cli import _databricks
|
|
12
|
+
from databricks_agentbricks.errors import AgentCliError
|
|
13
|
+
|
|
14
|
+
_BRANCH = "production"
|
|
15
|
+
_DATABASE = "databricks-postgres"
|
|
16
|
+
_ENDPOINT = "primary"
|
|
17
|
+
_RESOURCE_NAME = "postgres-runtime-store"
|
|
18
|
+
_SCHEMA_PREFIX = "databricks_agentkit_runtime_"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def backend(app: str) -> LakebaseBackend:
|
|
22
|
+
"""Return the dedicated per-app backend used before the managed API rollout."""
|
|
23
|
+
return LakebaseBackend(
|
|
24
|
+
project=_project_id(app),
|
|
25
|
+
branch=_BRANCH,
|
|
26
|
+
endpoint_id=_ENDPOINT,
|
|
27
|
+
database=_DATABASE,
|
|
28
|
+
schema=get_lakebase_schema(app),
|
|
29
|
+
tables=(),
|
|
30
|
+
resource_name=_RESOURCE_NAME,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def get_or_create_backend(app: str, profile: Optional[str]) -> LakebaseBackend:
|
|
35
|
+
"""Reuse or create the dedicated per-app project used by the legacy path."""
|
|
36
|
+
selected = backend(app)
|
|
37
|
+
project_path = f"projects/{selected.project}"
|
|
38
|
+
existing = _databricks(
|
|
39
|
+
["postgres", "get-project", project_path], profile, capture=True, check=False
|
|
40
|
+
)
|
|
41
|
+
if existing.returncode == 0:
|
|
42
|
+
return selected
|
|
43
|
+
|
|
44
|
+
payload = {"spec": {"display_name": f"Agent Bricks Runtime Store for {app}"}}
|
|
45
|
+
created = _databricks(
|
|
46
|
+
["postgres", "create-project", selected.project, "--json", json.dumps(payload)],
|
|
47
|
+
profile,
|
|
48
|
+
capture=True,
|
|
49
|
+
check=False,
|
|
50
|
+
)
|
|
51
|
+
if created.returncode == 0:
|
|
52
|
+
return selected
|
|
53
|
+
|
|
54
|
+
# A concurrent deploy can win the create race. Resolve the project before surfacing failure.
|
|
55
|
+
resolved = _databricks(
|
|
56
|
+
["postgres", "get-project", project_path], profile, capture=True, check=False
|
|
57
|
+
)
|
|
58
|
+
if resolved.returncode == 0:
|
|
59
|
+
return selected
|
|
60
|
+
detail = (created.stderr or created.stdout or "").strip() or "unknown error"
|
|
61
|
+
raise AgentCliError(f"Could not create Runtime Store '{selected.project}'.", hint=detail)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _project_id(app: str) -> str:
|
|
65
|
+
normalized = re.sub(r"[^a-z0-9-]+", "-", app.lower()).strip("-") or "agentbricks-app"
|
|
66
|
+
if not normalized[0].isalpha():
|
|
67
|
+
normalized = f"agentbricks-{normalized}"
|
|
68
|
+
return f"{normalized}-runtime-store"[:63].rstrip("-")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def get_lakebase_schema(app: str) -> str:
|
|
72
|
+
"""Return the per-app Runtime Store schema name."""
|
|
73
|
+
digest = hashlib.sha256(app.encode("utf-8")).hexdigest()[:12]
|
|
74
|
+
return f"{_SCHEMA_PREFIX}{digest}"
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""Persistent Agent Bricks project metadata and legacy framework detection."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import pathlib
|
|
7
|
+
import re
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
import tomli
|
|
12
|
+
|
|
13
|
+
from databricks_agentbricks.errors import AgentCliError
|
|
14
|
+
from databricks_agentbricks.project_types import AgentFramework, parse_framework
|
|
15
|
+
|
|
16
|
+
_CONFIG_PATH = pathlib.Path(".agentbricks/project.toml")
|
|
17
|
+
_SCHEMA_VERSION = 1
|
|
18
|
+
_CUSTOM_SERVER_TEMPLATES = frozenset({"custom-agent-langgraph", "custom-agent-openai"})
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True)
|
|
22
|
+
class ProjectMetadata:
|
|
23
|
+
"""Template identity persisted by ``ab init``."""
|
|
24
|
+
|
|
25
|
+
framework: AgentFramework
|
|
26
|
+
template: str | None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def write_project_metadata(
|
|
30
|
+
project: pathlib.Path,
|
|
31
|
+
*,
|
|
32
|
+
framework: str,
|
|
33
|
+
template: str,
|
|
34
|
+
) -> pathlib.Path:
|
|
35
|
+
"""Write the metadata consumed by template-aware Agent Bricks commands."""
|
|
36
|
+
selected_framework = parse_framework(framework)
|
|
37
|
+
target = project / _CONFIG_PATH
|
|
38
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
39
|
+
target.write_text(
|
|
40
|
+
f"schema_version = {_SCHEMA_VERSION}\n"
|
|
41
|
+
f"framework = {json.dumps(selected_framework.value)}\n"
|
|
42
|
+
f"template = {json.dumps(template)}\n",
|
|
43
|
+
encoding="utf-8",
|
|
44
|
+
)
|
|
45
|
+
return target
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _read_toml(path: pathlib.Path, description: str) -> dict[str, Any]:
|
|
49
|
+
try:
|
|
50
|
+
with path.open("rb") as input_file:
|
|
51
|
+
value = tomli.load(input_file)
|
|
52
|
+
except (OSError, tomli.TOMLDecodeError) as exc:
|
|
53
|
+
raise AgentCliError(f"Could not read {description} at {path}: {exc}.") from exc
|
|
54
|
+
if not isinstance(value, dict):
|
|
55
|
+
raise AgentCliError(f"{description.capitalize()} at {path} must be a TOML table.")
|
|
56
|
+
return value
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _load_persisted_metadata(project: pathlib.Path) -> ProjectMetadata | None:
|
|
60
|
+
path = project / _CONFIG_PATH
|
|
61
|
+
if not path.is_file():
|
|
62
|
+
return None
|
|
63
|
+
data = _read_toml(path, "Agent Bricks project config")
|
|
64
|
+
if data.get("schema_version") != _SCHEMA_VERSION:
|
|
65
|
+
raise AgentCliError(
|
|
66
|
+
f"Unsupported Agent Bricks project config schema in {path}.",
|
|
67
|
+
hint=f"Expected schema_version = {_SCHEMA_VERSION}.",
|
|
68
|
+
)
|
|
69
|
+
framework = parse_framework(data.get("framework"))
|
|
70
|
+
template = data.get("template")
|
|
71
|
+
if not isinstance(template, str) or not template:
|
|
72
|
+
raise AgentCliError(f"Agent Bricks project config at {path} must declare a template.")
|
|
73
|
+
return ProjectMetadata(framework=framework, template=template)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _dependency_name(requirement: str) -> str:
|
|
77
|
+
"""Return a normalized distribution name from a PEP 508 requirement."""
|
|
78
|
+
name = re.split(r"[\s\[<>=!~;@]", requirement.strip(), maxsplit=1)[0]
|
|
79
|
+
return name.lower().replace("_", "-")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _infer_legacy_framework(project: pathlib.Path) -> ProjectMetadata:
|
|
83
|
+
pyproject = project / "pyproject.toml"
|
|
84
|
+
if not pyproject.is_file():
|
|
85
|
+
raise AgentCliError(
|
|
86
|
+
f"Could not determine the Agent Bricks framework for {project}.",
|
|
87
|
+
hint="Run `ab init` to create project metadata or pass `--framework`.",
|
|
88
|
+
)
|
|
89
|
+
data = _read_toml(pyproject, "pyproject")
|
|
90
|
+
project_table = data.get("project")
|
|
91
|
+
dependencies = project_table.get("dependencies", []) if isinstance(project_table, dict) else []
|
|
92
|
+
if not isinstance(dependencies, list) or not all(
|
|
93
|
+
isinstance(item, str) for item in dependencies
|
|
94
|
+
):
|
|
95
|
+
dependencies = []
|
|
96
|
+
packages = {_dependency_name(item) for item in dependencies}
|
|
97
|
+
candidates = {
|
|
98
|
+
framework
|
|
99
|
+
for package, framework in (
|
|
100
|
+
("databricks-openai", AgentFramework.OPENAI),
|
|
101
|
+
("databricks-langchain", AgentFramework.LANGGRAPH),
|
|
102
|
+
)
|
|
103
|
+
if package in packages
|
|
104
|
+
}
|
|
105
|
+
if len(candidates) != 1:
|
|
106
|
+
detail = (
|
|
107
|
+
"both framework dependencies are present"
|
|
108
|
+
if candidates
|
|
109
|
+
else "no framework dependency was found"
|
|
110
|
+
)
|
|
111
|
+
raise AgentCliError(
|
|
112
|
+
f"Could not determine the Agent Bricks framework for {project}: {detail}.",
|
|
113
|
+
hint="Pass `--framework openai` or `--framework langgraph`.",
|
|
114
|
+
)
|
|
115
|
+
return ProjectMetadata(framework=candidates.pop(), template=None)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def load_project_metadata(
|
|
119
|
+
project: pathlib.Path,
|
|
120
|
+
*,
|
|
121
|
+
framework_override: str | None = None,
|
|
122
|
+
) -> ProjectMetadata:
|
|
123
|
+
"""Load init metadata, with dependency inference for projects created before it existed."""
|
|
124
|
+
persisted = _load_persisted_metadata(project)
|
|
125
|
+
if framework_override is not None:
|
|
126
|
+
override = parse_framework(framework_override)
|
|
127
|
+
if persisted is not None and persisted.framework != override:
|
|
128
|
+
raise AgentCliError(
|
|
129
|
+
f"Framework override '{override.value}' conflicts with {persisted.framework.value!r} in "
|
|
130
|
+
f"{project / _CONFIG_PATH}."
|
|
131
|
+
)
|
|
132
|
+
return persisted or ProjectMetadata(framework=override, template=None)
|
|
133
|
+
return persisted or _infer_legacy_framework(project)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def is_custom_server_template(template: str | None) -> bool:
|
|
137
|
+
"""Whether ``template`` is one of the Agent Bricks custom HTTP server templates."""
|
|
138
|
+
return template in _CUSTOM_SERVER_TEMPLATES
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def uses_custom_server(project: pathlib.Path) -> bool:
|
|
142
|
+
"""Whether persisted project metadata selects an Agent Bricks custom server template."""
|
|
143
|
+
metadata_path = project / _CONFIG_PATH
|
|
144
|
+
if not metadata_path.is_file():
|
|
145
|
+
return False
|
|
146
|
+
return is_custom_server_template(load_project_metadata(project).template)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def require_managed_tool_support(project: pathlib.Path) -> None:
|
|
150
|
+
"""Reject managed tool bindings for known templates that do not consume them."""
|
|
151
|
+
if not uses_custom_server(project):
|
|
152
|
+
return
|
|
153
|
+
raise AgentCliError(
|
|
154
|
+
"Managed tool bindings in agent.toml require an Agent Bricks server template.",
|
|
155
|
+
hint="Wire tools directly in agent/agent.py, or create a project with "
|
|
156
|
+
"`ab init --server agentbricks`.",
|
|
157
|
+
)
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Framework and server selections shared by the Agent Bricks CLI and project manifests."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from enum import Enum
|
|
6
|
+
|
|
7
|
+
from databricks_agentbricks.errors import AgentCliError
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class AgentFramework(str, Enum):
|
|
11
|
+
"""Frameworks supported by Agent Bricks project templates and adapters."""
|
|
12
|
+
|
|
13
|
+
LANGGRAPH = "langgraph"
|
|
14
|
+
OPENAI = "openai"
|
|
15
|
+
|
|
16
|
+
def __str__(self) -> str:
|
|
17
|
+
return self.value
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class AgentServer(str, Enum):
|
|
21
|
+
"""HTTP server ownership selected when an Agent Bricks project is created."""
|
|
22
|
+
|
|
23
|
+
AGENTBRICKS = "agentbricks"
|
|
24
|
+
CUSTOM = "custom"
|
|
25
|
+
|
|
26
|
+
def __str__(self) -> str:
|
|
27
|
+
return self.value
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def parse_framework(value: object) -> AgentFramework:
|
|
31
|
+
"""Validate a framework selection without exposing Enum errors to CLI users."""
|
|
32
|
+
if isinstance(value, str):
|
|
33
|
+
try:
|
|
34
|
+
return AgentFramework(value)
|
|
35
|
+
except ValueError:
|
|
36
|
+
pass
|
|
37
|
+
rendered = repr(str(value)) if isinstance(value, str) else "missing"
|
|
38
|
+
raise AgentCliError(
|
|
39
|
+
f"Unsupported Agent Bricks framework {rendered}.",
|
|
40
|
+
hint=f"Supported frameworks: {', '.join(framework.value for framework in AgentFramework)}.",
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def parse_server(value: object) -> AgentServer:
|
|
45
|
+
"""Validate a server selection without exposing Enum errors to CLI users."""
|
|
46
|
+
if isinstance(value, str):
|
|
47
|
+
try:
|
|
48
|
+
return AgentServer(value)
|
|
49
|
+
except ValueError:
|
|
50
|
+
pass
|
|
51
|
+
rendered = repr(str(value)) if isinstance(value, str) else "missing"
|
|
52
|
+
raise AgentCliError(
|
|
53
|
+
f"Unsupported Agent Bricks server {rendered}.",
|
|
54
|
+
hint=f"Supported servers: {', '.join(server.value for server in AgentServer)}.",
|
|
55
|
+
)
|
|
File without changes
|