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.
Files changed (159) hide show
  1. databricks_agentbricks/__init__.py +1 -0
  2. databricks_agentbricks/_group.py +217 -0
  3. databricks_agentbricks/agent_project.py +657 -0
  4. databricks_agentbricks/app_resources.py +139 -0
  5. databricks_agentbricks/cli/__init__.py +12 -0
  6. databricks_agentbricks/cli/app.py +108 -0
  7. databricks_agentbricks/cli/app_auth.py +198 -0
  8. databricks_agentbricks/cli/auth.py +142 -0
  9. databricks_agentbricks/cli/deploy.py +954 -0
  10. databricks_agentbricks/cli/dev.py +310 -0
  11. databricks_agentbricks/cli/endpoint.py +145 -0
  12. databricks_agentbricks/cli/endpoint_examples.py +33 -0
  13. databricks_agentbricks/cli/endpoint_output.py +53 -0
  14. databricks_agentbricks/cli/endpoint_request.py +71 -0
  15. databricks_agentbricks/cli/endpoint_transport.py +148 -0
  16. databricks_agentbricks/cli/help.py +467 -0
  17. databricks_agentbricks/cli/init.py +496 -0
  18. databricks_agentbricks/cli/mcp.py +75 -0
  19. databricks_agentbricks/cli/memory.py +520 -0
  20. databricks_agentbricks/cli/sandbox.py +823 -0
  21. databricks_agentbricks/cli/sessions.py +588 -0
  22. databricks_agentbricks/cli/tools.py +456 -0
  23. databricks_agentbricks/cli/tracing.py +724 -0
  24. databricks_agentbricks/databricks_cli.py +38 -0
  25. databricks_agentbricks/errors.py +114 -0
  26. databricks_agentbricks/lakebase_runtime_store.py +107 -0
  27. databricks_agentbricks/legacy_lakebase_runtime_store.py +74 -0
  28. databricks_agentbricks/project_config.py +157 -0
  29. databricks_agentbricks/project_types.py +55 -0
  30. databricks_agentbricks/py.typed +0 -0
  31. databricks_agentbricks/render.py +349 -0
  32. databricks_agentbricks/templates/agent-bricks-migrate/SKILL.md +101 -0
  33. databricks_agentbricks/templates/agent-langgraph/.env.example +36 -0
  34. databricks_agentbricks/templates/agent-langgraph/.gitignore +199 -0
  35. databricks_agentbricks/templates/agent-langgraph/AGENTKIT_CONTRACT.md +95 -0
  36. databricks_agentbricks/templates/agent-langgraph/AGENTS.md +31 -0
  37. databricks_agentbricks/templates/agent-langgraph/CLAUDE.md +1 -0
  38. databricks_agentbricks/templates/agent-langgraph/README.md +141 -0
  39. databricks_agentbricks/templates/agent-langgraph/agent/__init__.py +0 -0
  40. databricks_agentbricks/templates/agent-langgraph/agent/agent.py +177 -0
  41. databricks_agentbricks/templates/agent-langgraph/agent/mcps.py +27 -0
  42. databricks_agentbricks/templates/agent-langgraph/agent/tools/__init__.py +24 -0
  43. databricks_agentbricks/templates/agent-langgraph/agent/tools/sample_tool.py +15 -0
  44. databricks_agentbricks/templates/agent-langgraph/agent/tools/send_message.py +17 -0
  45. databricks_agentbricks/templates/agent-langgraph/app.yaml +15 -0
  46. databricks_agentbricks/templates/agent-langgraph/pyproject.toml +42 -0
  47. databricks_agentbricks/templates/agent-langgraph/runtime/__init__.py +0 -0
  48. databricks_agentbricks/templates/agent-langgraph/runtime/adapter.py +144 -0
  49. databricks_agentbricks/templates/agent-langgraph/runtime/main.py +25 -0
  50. databricks_agentbricks/templates/agent-langgraph/tests/__init__.py +0 -0
  51. databricks_agentbricks/templates/agent-langgraph/tests/test_agent.py +211 -0
  52. databricks_agentbricks/templates/agent-langgraph/tests/test_runtime.py +19 -0
  53. databricks_agentbricks/templates/agent-langgraph/tests/test_workspace.py +28 -0
  54. databricks_agentbricks/templates/agent-openai/.env.example +37 -0
  55. databricks_agentbricks/templates/agent-openai/.gitignore +199 -0
  56. databricks_agentbricks/templates/agent-openai/AGENTKIT_CONTRACT.md +107 -0
  57. databricks_agentbricks/templates/agent-openai/AGENTS.md +76 -0
  58. databricks_agentbricks/templates/agent-openai/CLAUDE.md +1 -0
  59. databricks_agentbricks/templates/agent-openai/README.md +143 -0
  60. databricks_agentbricks/templates/agent-openai/agent/__init__.py +0 -0
  61. databricks_agentbricks/templates/agent-openai/agent/agent.py +205 -0
  62. databricks_agentbricks/templates/agent-openai/agent/mcps.py +28 -0
  63. databricks_agentbricks/templates/agent-openai/agent/tools/__init__.py +24 -0
  64. databricks_agentbricks/templates/agent-openai/agent/tools/sample_tool.py +15 -0
  65. databricks_agentbricks/templates/agent-openai/agent/tools/send_message.py +18 -0
  66. databricks_agentbricks/templates/agent-openai/app.yaml +15 -0
  67. databricks_agentbricks/templates/agent-openai/pyproject.toml +42 -0
  68. databricks_agentbricks/templates/agent-openai/runtime/__init__.py +0 -0
  69. databricks_agentbricks/templates/agent-openai/runtime/adapter.py +188 -0
  70. databricks_agentbricks/templates/agent-openai/runtime/main.py +25 -0
  71. databricks_agentbricks/templates/agent-openai/tests/__init__.py +0 -0
  72. databricks_agentbricks/templates/agent-openai/tests/test_agent.py +326 -0
  73. databricks_agentbricks/templates/agent-openai/tests/test_runtime.py +19 -0
  74. databricks_agentbricks/templates/agent-openai/tests/test_workspace.py +28 -0
  75. databricks_agentbricks/templates/custom-agent-langgraph/.env.example +1 -0
  76. databricks_agentbricks/templates/custom-agent-langgraph/.gitignore +11 -0
  77. databricks_agentbricks/templates/custom-agent-langgraph/README.md +22 -0
  78. databricks_agentbricks/templates/custom-agent-langgraph/agent/__init__.py +0 -0
  79. databricks_agentbricks/templates/custom-agent-langgraph/agent/agent.py +27 -0
  80. databricks_agentbricks/templates/custom-agent-langgraph/app.yaml +1 -0
  81. databricks_agentbricks/templates/custom-agent-langgraph/pyproject.toml +38 -0
  82. databricks_agentbricks/templates/custom-agent-langgraph/runtime/__init__.py +0 -0
  83. databricks_agentbricks/templates/custom-agent-langgraph/runtime/main.py +31 -0
  84. databricks_agentbricks/templates/custom-agent-langgraph/tests/test_runtime.py +29 -0
  85. databricks_agentbricks/templates/custom-agent-openai/.env.example +1 -0
  86. databricks_agentbricks/templates/custom-agent-openai/.gitignore +11 -0
  87. databricks_agentbricks/templates/custom-agent-openai/README.md +22 -0
  88. databricks_agentbricks/templates/custom-agent-openai/agent/__init__.py +0 -0
  89. databricks_agentbricks/templates/custom-agent-openai/agent/agent.py +29 -0
  90. databricks_agentbricks/templates/custom-agent-openai/app.yaml +1 -0
  91. databricks_agentbricks/templates/custom-agent-openai/pyproject.toml +38 -0
  92. databricks_agentbricks/templates/custom-agent-openai/runtime/__init__.py +0 -0
  93. databricks_agentbricks/templates/custom-agent-openai/runtime/main.py +31 -0
  94. databricks_agentbricks/templates/custom-agent-openai/tests/test_runtime.py +29 -0
  95. databricks_agentbricks/templates/sandbox_mcp.py +39 -0
  96. databricks_agentbricks/templates/sandbox_mcp_langgraph.py +39 -0
  97. databricks_agentbricks/templates/ui/agent-langgraph/CHAT_APP.md +49 -0
  98. databricks_agentbricks/templates/ui/agent-langgraph/runtime/main.py +26 -0
  99. databricks_agentbricks/templates/ui/agent-langgraph/runtime/ui.py +528 -0
  100. databricks_agentbricks/templates/ui/agent-langgraph/tests/test_demo_ui.py +542 -0
  101. databricks_agentbricks/templates/ui/agent-langgraph/ui/agentbricks-mark.png +0 -0
  102. databricks_agentbricks/templates/ui/agent-langgraph/ui/app.js +1464 -0
  103. databricks_agentbricks/templates/ui/agent-langgraph/ui/index.html +281 -0
  104. databricks_agentbricks/templates/ui/agent-langgraph/ui/styles.css +1913 -0
  105. databricks_agentbricks/templates/ui/agent-openai/CHAT_APP.md +54 -0
  106. databricks_agentbricks/templates/ui/agent-openai/runtime/main.py +26 -0
  107. databricks_agentbricks/templates/ui/agent-openai/runtime/ui.py +521 -0
  108. databricks_agentbricks/templates/ui/agent-openai/tests/test_demo_ui.py +512 -0
  109. databricks_agentbricks/templates/ui/agent-openai/ui/agentbricks-mark.png +0 -0
  110. databricks_agentbricks/templates/ui/agent-openai/ui/app.js +1428 -0
  111. databricks_agentbricks/templates/ui/agent-openai/ui/index.html +281 -0
  112. databricks_agentbricks/templates/ui/agent-openai/ui/styles.css +1913 -0
  113. databricks_agentbricks/theme.py +72 -0
  114. databricks_agentbricks-0.2.0.dist-info/METADATA +897 -0
  115. databricks_agentbricks-0.2.0.dist-info/RECORD +159 -0
  116. databricks_agentbricks-0.2.0.dist-info/WHEEL +4 -0
  117. databricks_agentbricks-0.2.0.dist-info/entry_points.txt +2 -0
  118. databricks_agentbricks-0.2.0.dist-info/licenses/NOTICE +39 -0
  119. databricks_agentkit/__init__.py +67 -0
  120. databricks_agentkit/_api_client.py +673 -0
  121. databricks_agentkit/_pagination.py +25 -0
  122. databricks_agentkit/client.py +31 -0
  123. databricks_agentkit/langgraph/__init__.py +98 -0
  124. databricks_agentkit/langgraph/genie.py +50 -0
  125. databricks_agentkit/langgraph/mcp.py +216 -0
  126. databricks_agentkit/langgraph/memory.py +67 -0
  127. databricks_agentkit/langgraph/session_store.py +485 -0
  128. databricks_agentkit/memory_store.py +371 -0
  129. databricks_agentkit/models.py +330 -0
  130. databricks_agentkit/openai/__init__.py +93 -0
  131. databricks_agentkit/openai/genie.py +50 -0
  132. databricks_agentkit/openai/mcp.py +189 -0
  133. databricks_agentkit/openai/memory.py +67 -0
  134. databricks_agentkit/openai/sessions.py +157 -0
  135. databricks_agentkit/py.typed +0 -0
  136. databricks_agentkit/runtime/ARCHITECTURE.md +64 -0
  137. databricks_agentkit/runtime/README.md +184 -0
  138. databricks_agentkit/runtime/__init__.py +69 -0
  139. databricks_agentkit/runtime/app.py +316 -0
  140. databricks_agentkit/runtime/auth.py +149 -0
  141. databricks_agentkit/runtime/durability/__init__.py +1 -0
  142. databricks_agentkit/runtime/durability/execution.py +133 -0
  143. databricks_agentkit/runtime/durability/heartbeat.py +64 -0
  144. databricks_agentkit/runtime/durability/lakebase_runtime_store.py +648 -0
  145. databricks_agentkit/runtime/durability/recovery.py +70 -0
  146. databricks_agentkit/runtime/durability/store.py +33 -0
  147. databricks_agentkit/runtime/execution.py +160 -0
  148. databricks_agentkit/runtime/genie.py +224 -0
  149. databricks_agentkit/runtime/mcp_auth.py +114 -0
  150. databricks_agentkit/runtime/model_services.py +90 -0
  151. databricks_agentkit/runtime/runtime.py +273 -0
  152. databricks_agentkit/runtime/session_store_client.py +120 -0
  153. databricks_agentkit/runtime/store.py +287 -0
  154. databricks_agentkit/runtime/tool_manifest.py +223 -0
  155. databricks_agentkit/runtime/tracing.py +71 -0
  156. databricks_agentkit/runtime/types.py +118 -0
  157. databricks_agentkit/runtime/workspace.py +27 -0
  158. databricks_agentkit/session_store.py +434 -0
  159. 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