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,673 @@
|
|
|
1
|
+
"""Private transport for the managed agent store APIs.
|
|
2
|
+
|
|
3
|
+
The public SDK is the resource-oriented :class:`databricks_agentkit.AgentKitClient`.
|
|
4
|
+
This module temporarily owns the one-method-per-endpoint transport used by that
|
|
5
|
+
wrapper and the CLI. It can be replaced by the generated ``WorkspaceClient.mason``
|
|
6
|
+
service without changing the public resource surface.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import configparser
|
|
12
|
+
import os
|
|
13
|
+
import pathlib
|
|
14
|
+
import time
|
|
15
|
+
from typing import TYPE_CHECKING, Any, Optional
|
|
16
|
+
from urllib.parse import quote
|
|
17
|
+
|
|
18
|
+
from databricks_agentbricks.errors import TRANSIENT_ERROR_CODES, AgentCliError, wrap_api_error
|
|
19
|
+
from databricks_agentkit import models
|
|
20
|
+
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
from databricks.sdk import WorkspaceClient
|
|
23
|
+
|
|
24
|
+
_BASE = "/api/2.0/agents"
|
|
25
|
+
_MCP_SERVICES_PATH = "/api/2.1/unity-catalog/mcp-services"
|
|
26
|
+
|
|
27
|
+
# Transient backend failures (e.g. a CANCELLED RPC) usually clear on a retry, so retry safe
|
|
28
|
+
# requests once before surfacing them. Mutating requests must opt in explicitly: their first
|
|
29
|
+
# attempt may have committed even when its response was lost.
|
|
30
|
+
_MAX_ATTEMPTS = 2
|
|
31
|
+
_RETRY_BASE_DELAY_S = 0.2
|
|
32
|
+
|
|
33
|
+
# Bound the SDK's own retry budget (default 300s). The SDK retries every HTTP 429/503 as platform
|
|
34
|
+
# throttling, so an interactive command otherwise hangs up to 5 minutes on a sustained 429 before
|
|
35
|
+
# surfacing anything; cap it so throttling fails fast with a clear error.
|
|
36
|
+
_CLI_RETRY_TIMEOUT_S = 60
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _query(**kwargs: Any) -> dict[str, Any]:
|
|
40
|
+
"""Build a query dict, dropping None and empty values."""
|
|
41
|
+
return {k: v for k, v in kwargs.items() if v is not None and v != ""}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _body(**kwargs: Any) -> dict[str, Any]:
|
|
45
|
+
"""Build a request body while retaining meaningful empty values."""
|
|
46
|
+
return {k: v for k, v in kwargs.items() if v is not None}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _as(cls: type, resp: Any) -> Any:
|
|
50
|
+
"""Wrap a JSON response in a typed model, passing non-dicts through unchanged."""
|
|
51
|
+
return cls(resp) if isinstance(resp, dict) else resp
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def memory_store_path(name: str) -> str:
|
|
55
|
+
"""Normalize a store id or name into the `memory-stores/{id}` resource segment.
|
|
56
|
+
|
|
57
|
+
Validate locally so malformed resource names do not produce misleading endpoint errors.
|
|
58
|
+
"""
|
|
59
|
+
raw = (name or "").strip()
|
|
60
|
+
if raw.startswith("memory-stores/"):
|
|
61
|
+
raw = raw[len("memory-stores/") :]
|
|
62
|
+
raw = raw.strip().strip("/")
|
|
63
|
+
if not raw:
|
|
64
|
+
raise AgentCliError("A memory store id or resource name is required.")
|
|
65
|
+
if "/" in raw:
|
|
66
|
+
raise AgentCliError(f"Invalid memory store id or resource name: {name!r}")
|
|
67
|
+
return f"memory-stores/{raw}"
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def session_store_path(name: str) -> str:
|
|
71
|
+
"""Normalize a session store name into the `session-stores/{name}` resource segment."""
|
|
72
|
+
raw = (name or "").strip()
|
|
73
|
+
if raw.startswith("session-stores/"):
|
|
74
|
+
raw = raw[len("session-stores/") :]
|
|
75
|
+
raw = raw.strip().strip("/")
|
|
76
|
+
if not raw:
|
|
77
|
+
raise AgentCliError("A session store name is required.")
|
|
78
|
+
return f"session-stores/{raw}"
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def memory_entry_path(store: str, entry: str) -> str:
|
|
82
|
+
entry = (entry or "").strip().strip("/")
|
|
83
|
+
if entry.startswith("memory-stores/"):
|
|
84
|
+
return entry
|
|
85
|
+
if not entry:
|
|
86
|
+
raise AgentCliError("A memory entry id or resource name is required.")
|
|
87
|
+
return f"{memory_store_path(store)}/entries/{entry}"
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _profile_host(profile: str) -> Optional[str]:
|
|
91
|
+
config_path = pathlib.Path(
|
|
92
|
+
os.getenv("DATABRICKS_CONFIG_FILE", pathlib.Path.home() / ".databrickscfg")
|
|
93
|
+
)
|
|
94
|
+
parser = configparser.ConfigParser()
|
|
95
|
+
try:
|
|
96
|
+
parser.read(config_path)
|
|
97
|
+
except (OSError, configparser.Error):
|
|
98
|
+
return None
|
|
99
|
+
return parser.get(profile, "host", fallback=None)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _bound_retry_timeout(client: WorkspaceClient) -> WorkspaceClient:
|
|
103
|
+
# Shorten the SDK's default 300s retry budget so 429/503 throttling fails in ~1 min, not ~5.
|
|
104
|
+
try:
|
|
105
|
+
client.api_client._api_client._retry_timeout_seconds = _CLI_RETRY_TIMEOUT_S
|
|
106
|
+
except AttributeError:
|
|
107
|
+
pass
|
|
108
|
+
return client
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _workspace_client(profile: Optional[str]) -> WorkspaceClient:
|
|
112
|
+
# Imported here (not at module top) so the ~0.7s databricks.sdk import is paid only when a
|
|
113
|
+
# command actually builds a client, not on every CLI invocation.
|
|
114
|
+
from databricks.sdk import WorkspaceClient
|
|
115
|
+
|
|
116
|
+
client = _bound_retry_timeout(WorkspaceClient(profile=profile))
|
|
117
|
+
if not profile or not client.config.workspace_id:
|
|
118
|
+
return client
|
|
119
|
+
|
|
120
|
+
configured_host = _profile_host(profile)
|
|
121
|
+
resolved_host = client.config.host
|
|
122
|
+
if not configured_host or configured_host.rstrip("/") == (resolved_host or "").rstrip("/"):
|
|
123
|
+
return client
|
|
124
|
+
|
|
125
|
+
workspace_id = str(client.config.workspace_id)
|
|
126
|
+
return _bound_retry_timeout(
|
|
127
|
+
WorkspaceClient(
|
|
128
|
+
profile=profile,
|
|
129
|
+
host=configured_host,
|
|
130
|
+
custom_headers={"X-Databricks-Org-Id": workspace_id},
|
|
131
|
+
)
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class _AgentBricksApiClient:
|
|
136
|
+
"""Private transport for the 2.0 agents API until the generated SDK is available."""
|
|
137
|
+
|
|
138
|
+
def __init__(
|
|
139
|
+
self,
|
|
140
|
+
profile: Optional[str] = None,
|
|
141
|
+
*,
|
|
142
|
+
workspace_client: Optional[WorkspaceClient] = None,
|
|
143
|
+
) -> None:
|
|
144
|
+
if profile is not None and workspace_client is not None:
|
|
145
|
+
raise ValueError("profile and workspace_client are mutually exclusive")
|
|
146
|
+
try:
|
|
147
|
+
self._w = workspace_client or _workspace_client(profile)
|
|
148
|
+
except Exception as exc: # noqa: BLE001 - surfaced as a clean CLI error
|
|
149
|
+
raise AgentCliError(
|
|
150
|
+
f"Could not initialize Databricks auth: {exc}",
|
|
151
|
+
hint="Select an existing profile with `ab --profile <name> <command>` "
|
|
152
|
+
"or authenticate and save it with `ab login --profile <name>`.",
|
|
153
|
+
) from exc
|
|
154
|
+
|
|
155
|
+
@property
|
|
156
|
+
def host(self) -> str:
|
|
157
|
+
return self._w.config.host or "unknown"
|
|
158
|
+
|
|
159
|
+
@property
|
|
160
|
+
def current_user(self) -> str:
|
|
161
|
+
"""The authenticated user's name (used to derive the app source workspace path)."""
|
|
162
|
+
return str(self._w.current_user.me().user_name or "unknown")
|
|
163
|
+
|
|
164
|
+
def ensure_workspace_dir(self, path: str) -> None:
|
|
165
|
+
"""Create a workspace directory (and parents), idempotently.
|
|
166
|
+
|
|
167
|
+
MLflow's create_experiment won't create the intermediate folder for a nested experiment
|
|
168
|
+
path, so callers make the parent dir first.
|
|
169
|
+
"""
|
|
170
|
+
self._w.workspace.mkdirs(path)
|
|
171
|
+
|
|
172
|
+
def create_runtime_store(
|
|
173
|
+
self,
|
|
174
|
+
runtime_store_id: str,
|
|
175
|
+
app_service_principal_id: str,
|
|
176
|
+
*,
|
|
177
|
+
app_name: str,
|
|
178
|
+
retry_transient: bool = False,
|
|
179
|
+
) -> models.RuntimeStore:
|
|
180
|
+
"""Create the deployment's Runtime Store through Conversation Store."""
|
|
181
|
+
return _as(
|
|
182
|
+
models.RuntimeStore,
|
|
183
|
+
self._do(
|
|
184
|
+
"POST",
|
|
185
|
+
f"{_BASE}/runtime-stores",
|
|
186
|
+
query={"runtime_store_id": runtime_store_id},
|
|
187
|
+
body={
|
|
188
|
+
"owner": {
|
|
189
|
+
"app": {
|
|
190
|
+
"name": app_name,
|
|
191
|
+
"service_principal_id": app_service_principal_id,
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
},
|
|
195
|
+
safe_to_retry=retry_transient,
|
|
196
|
+
),
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
def get_runtime_store(self, runtime_store_id: str) -> models.RuntimeStore:
|
|
200
|
+
"""Resolve the service-managed backend and app owner before reusing a store."""
|
|
201
|
+
return _as(
|
|
202
|
+
models.RuntimeStore,
|
|
203
|
+
self._do("GET", f"{_BASE}/runtime-stores/{quote(runtime_store_id, safe='')}"),
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
def delete_runtime_store(self, runtime_store_id: str) -> dict:
|
|
207
|
+
"""Delete a deployment's Runtime Store and its dedicated database."""
|
|
208
|
+
return self._do(
|
|
209
|
+
"DELETE",
|
|
210
|
+
f"{_BASE}/runtime-stores/{quote(runtime_store_id, safe='')}",
|
|
211
|
+
safe_to_retry=True,
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
def _do(
|
|
215
|
+
self,
|
|
216
|
+
method: str,
|
|
217
|
+
path: str,
|
|
218
|
+
*,
|
|
219
|
+
query: Optional[dict] = None,
|
|
220
|
+
body: Optional[dict] = None,
|
|
221
|
+
safe_to_retry: bool = False,
|
|
222
|
+
) -> Any:
|
|
223
|
+
retry_allowed = method == "GET" or safe_to_retry
|
|
224
|
+
delay = _RETRY_BASE_DELAY_S
|
|
225
|
+
for attempt in range(1, _MAX_ATTEMPTS + 1):
|
|
226
|
+
try:
|
|
227
|
+
return self._w.api_client.do(method, path, query=query, body=body)
|
|
228
|
+
except Exception as exc: # noqa: BLE001 - normalized to AgentCliError
|
|
229
|
+
retryable = (
|
|
230
|
+
retry_allowed and getattr(exc, "error_code", None) in TRANSIENT_ERROR_CODES
|
|
231
|
+
)
|
|
232
|
+
if not retryable or attempt == _MAX_ATTEMPTS:
|
|
233
|
+
raise wrap_api_error(exc) from exc
|
|
234
|
+
time.sleep(delay)
|
|
235
|
+
delay *= 2
|
|
236
|
+
|
|
237
|
+
# --- Unity Catalog MCP Services -----------------------------------------
|
|
238
|
+
|
|
239
|
+
def get_mcp_service(self, service: str) -> dict:
|
|
240
|
+
"""Look up a managed MCP service visible to the authenticated user."""
|
|
241
|
+
return self._do("GET", f"{_MCP_SERVICES_PATH}/{quote(service, safe='')}")
|
|
242
|
+
|
|
243
|
+
def list_mcp_services(
|
|
244
|
+
self, schema: str = "system.ai", page_token: Optional[str] = None
|
|
245
|
+
) -> dict:
|
|
246
|
+
"""List MCP Services visible to the user in a Unity Catalog schema."""
|
|
247
|
+
return self._do(
|
|
248
|
+
"GET",
|
|
249
|
+
_MCP_SERVICES_PATH,
|
|
250
|
+
query=_query(parent=f"schemas/{schema}", page_token=page_token),
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
# --- memory stores -------------------------------------------------------
|
|
254
|
+
|
|
255
|
+
def create_memory_store(
|
|
256
|
+
self,
|
|
257
|
+
display_name: str,
|
|
258
|
+
description: Optional[str] = None,
|
|
259
|
+
*,
|
|
260
|
+
retry_transient: bool = False,
|
|
261
|
+
) -> models.MemoryStore:
|
|
262
|
+
body = _body(display_name=display_name, description=description)
|
|
263
|
+
return _as(
|
|
264
|
+
models.MemoryStore,
|
|
265
|
+
self._do(
|
|
266
|
+
"POST",
|
|
267
|
+
f"{_BASE}/memory-stores",
|
|
268
|
+
query={"managed_memory_store_id": display_name},
|
|
269
|
+
body=body,
|
|
270
|
+
safe_to_retry=retry_transient,
|
|
271
|
+
),
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
def get_memory_store(self, name: str) -> models.MemoryStore:
|
|
275
|
+
return _as(models.MemoryStore, self._do("GET", f"{_BASE}/{memory_store_path(name)}"))
|
|
276
|
+
|
|
277
|
+
def list_memory_stores(
|
|
278
|
+
self, page_size: Optional[int] = None, page_token: Optional[str] = None
|
|
279
|
+
) -> models.MemoryStoreList:
|
|
280
|
+
return _as(
|
|
281
|
+
models.MemoryStoreList,
|
|
282
|
+
self._do(
|
|
283
|
+
"GET",
|
|
284
|
+
f"{_BASE}/memory-stores",
|
|
285
|
+
query=_query(page_size=page_size, page_token=page_token),
|
|
286
|
+
),
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
def update_memory_store(
|
|
290
|
+
self, name: str, display_name: Optional[str] = None, description: Optional[str] = None
|
|
291
|
+
) -> models.MemoryStore:
|
|
292
|
+
body = _body(display_name=display_name, description=description)
|
|
293
|
+
if not body:
|
|
294
|
+
raise AgentCliError("No fields to update. Provide a display name and/or description.")
|
|
295
|
+
mask = ",".join(body.keys())
|
|
296
|
+
return _as(
|
|
297
|
+
models.MemoryStore,
|
|
298
|
+
self._do(
|
|
299
|
+
"PATCH",
|
|
300
|
+
f"{_BASE}/{memory_store_path(name)}",
|
|
301
|
+
query=_query(update_mask=mask),
|
|
302
|
+
body=body,
|
|
303
|
+
),
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
def delete_memory_store(self, name: str) -> dict:
|
|
307
|
+
return self._do("DELETE", f"{_BASE}/{memory_store_path(name)}")
|
|
308
|
+
|
|
309
|
+
# --- memory entries ------------------------------------------------------
|
|
310
|
+
|
|
311
|
+
def create_memory_entry(
|
|
312
|
+
self,
|
|
313
|
+
store: str,
|
|
314
|
+
actor_id: str,
|
|
315
|
+
path: str,
|
|
316
|
+
content: Optional[str] = None,
|
|
317
|
+
description: Optional[str] = None,
|
|
318
|
+
session_id: Optional[str] = None,
|
|
319
|
+
source_type: Optional[str] = None,
|
|
320
|
+
write_mode: Optional[str] = None,
|
|
321
|
+
) -> models.MemoryEntry:
|
|
322
|
+
body = _body(
|
|
323
|
+
actor_id=actor_id,
|
|
324
|
+
path=path,
|
|
325
|
+
content=content,
|
|
326
|
+
description=description,
|
|
327
|
+
session_id=session_id,
|
|
328
|
+
source_type=source_type,
|
|
329
|
+
write_mode=write_mode,
|
|
330
|
+
)
|
|
331
|
+
return _as(
|
|
332
|
+
models.MemoryEntry,
|
|
333
|
+
self._do("POST", f"{_BASE}/{memory_store_path(store)}/entries", body=body),
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
def get_memory_entry(
|
|
337
|
+
self, store: str, entry: str, read_mask: Optional[str] = None
|
|
338
|
+
) -> models.MemoryEntry:
|
|
339
|
+
return _as(
|
|
340
|
+
models.MemoryEntry,
|
|
341
|
+
self._do(
|
|
342
|
+
"GET",
|
|
343
|
+
f"{_BASE}/{memory_entry_path(store, entry)}",
|
|
344
|
+
query=_query(read_mask=read_mask),
|
|
345
|
+
),
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
def list_memory_entries(
|
|
349
|
+
self,
|
|
350
|
+
store: str,
|
|
351
|
+
actor_id: str,
|
|
352
|
+
path_prefix: Optional[str] = None,
|
|
353
|
+
session_id: Optional[str] = None,
|
|
354
|
+
page_size: Optional[int] = None,
|
|
355
|
+
page_token: Optional[str] = None,
|
|
356
|
+
read_mask: Optional[str] = None,
|
|
357
|
+
) -> models.MemoryEntryList:
|
|
358
|
+
return _as(
|
|
359
|
+
models.MemoryEntryList,
|
|
360
|
+
self._do(
|
|
361
|
+
"GET",
|
|
362
|
+
f"{_BASE}/{memory_store_path(store)}/entries",
|
|
363
|
+
query=_query(
|
|
364
|
+
actor_id=actor_id,
|
|
365
|
+
path_prefix=path_prefix,
|
|
366
|
+
session_id=session_id,
|
|
367
|
+
page_size=page_size,
|
|
368
|
+
page_token=page_token,
|
|
369
|
+
read_mask=read_mask,
|
|
370
|
+
),
|
|
371
|
+
),
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
def search_memory_entries(
|
|
375
|
+
self,
|
|
376
|
+
store: str,
|
|
377
|
+
actor_id: str,
|
|
378
|
+
query: str,
|
|
379
|
+
limit: Optional[int] = None,
|
|
380
|
+
page_size: Optional[int] = None,
|
|
381
|
+
path_prefix: Optional[str] = None,
|
|
382
|
+
session_id: Optional[str] = None,
|
|
383
|
+
read_mask: Optional[str] = None,
|
|
384
|
+
) -> models.MemorySearchResult:
|
|
385
|
+
body = _body(
|
|
386
|
+
actor_id=actor_id,
|
|
387
|
+
query=query,
|
|
388
|
+
limit=limit,
|
|
389
|
+
page_size=page_size,
|
|
390
|
+
path_prefix=path_prefix,
|
|
391
|
+
session_id=session_id,
|
|
392
|
+
read_mask=read_mask,
|
|
393
|
+
)
|
|
394
|
+
return _as(
|
|
395
|
+
models.MemorySearchResult,
|
|
396
|
+
self._do(
|
|
397
|
+
"POST",
|
|
398
|
+
f"{_BASE}/{memory_store_path(store)}/entries:search",
|
|
399
|
+
body=body,
|
|
400
|
+
safe_to_retry=True,
|
|
401
|
+
),
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
def update_memory_entry(
|
|
405
|
+
self,
|
|
406
|
+
store: str,
|
|
407
|
+
entry: str,
|
|
408
|
+
content: Optional[str] = None,
|
|
409
|
+
description: Optional[str] = None,
|
|
410
|
+
) -> models.MemoryEntry:
|
|
411
|
+
body = _body(content=content, description=description)
|
|
412
|
+
if not body:
|
|
413
|
+
raise AgentCliError("No fields to update. Provide content and/or a description.")
|
|
414
|
+
mask = ",".join(body.keys())
|
|
415
|
+
return _as(
|
|
416
|
+
models.MemoryEntry,
|
|
417
|
+
self._do(
|
|
418
|
+
"PATCH",
|
|
419
|
+
f"{_BASE}/{memory_entry_path(store, entry)}",
|
|
420
|
+
query=_query(update_mask=mask),
|
|
421
|
+
body=body,
|
|
422
|
+
),
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
def delete_memory_entry(self, store: str, entry: str) -> dict:
|
|
426
|
+
return self._do("DELETE", f"{_BASE}/{memory_entry_path(store, entry)}")
|
|
427
|
+
|
|
428
|
+
# --- session stores ------------------------------------------------------
|
|
429
|
+
|
|
430
|
+
def create_session_store(
|
|
431
|
+
self,
|
|
432
|
+
name: str,
|
|
433
|
+
description: Optional[str] = None,
|
|
434
|
+
metadata: Optional[dict] = None,
|
|
435
|
+
*,
|
|
436
|
+
retry_transient: bool = False,
|
|
437
|
+
) -> models.SessionStore:
|
|
438
|
+
body = _body(description=description, metadata=metadata)
|
|
439
|
+
return _as(
|
|
440
|
+
models.SessionStore,
|
|
441
|
+
self._do(
|
|
442
|
+
"POST",
|
|
443
|
+
f"{_BASE}/session-stores",
|
|
444
|
+
query={"session_store_name": name, "session_store_id": name},
|
|
445
|
+
body=body,
|
|
446
|
+
safe_to_retry=retry_transient,
|
|
447
|
+
),
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
def get_session_store(self, name: str) -> models.SessionStore:
|
|
451
|
+
return _as(models.SessionStore, self._do("GET", f"{_BASE}/{session_store_path(name)}"))
|
|
452
|
+
|
|
453
|
+
def list_session_stores(
|
|
454
|
+
self, page_size: Optional[int] = None, page_token: Optional[str] = None
|
|
455
|
+
) -> models.SessionStoreList:
|
|
456
|
+
return _as(
|
|
457
|
+
models.SessionStoreList,
|
|
458
|
+
self._do(
|
|
459
|
+
"GET",
|
|
460
|
+
f"{_BASE}/session-stores",
|
|
461
|
+
query=_query(page_size=page_size, page_token=page_token),
|
|
462
|
+
),
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
def update_session_store(
|
|
466
|
+
self, name: str, description: Optional[str] = None, metadata: Optional[dict] = None
|
|
467
|
+
) -> models.SessionStore:
|
|
468
|
+
body = _body(description=description, metadata=metadata)
|
|
469
|
+
if not body:
|
|
470
|
+
raise AgentCliError("No fields to update. Provide a description and/or metadata.")
|
|
471
|
+
mask = ",".join(body.keys())
|
|
472
|
+
return _as(
|
|
473
|
+
models.SessionStore,
|
|
474
|
+
self._do(
|
|
475
|
+
"PATCH",
|
|
476
|
+
f"{_BASE}/{session_store_path(name)}",
|
|
477
|
+
query=_query(update_mask=mask),
|
|
478
|
+
body=body,
|
|
479
|
+
),
|
|
480
|
+
)
|
|
481
|
+
|
|
482
|
+
def delete_session_store(self, name: str) -> dict:
|
|
483
|
+
return self._do("DELETE", f"{_BASE}/{session_store_path(name)}")
|
|
484
|
+
|
|
485
|
+
# --- store permission grants --------------------------------------------
|
|
486
|
+
|
|
487
|
+
def grant_session_store_permission(
|
|
488
|
+
self, name: str, principal_client_id: str, permission: str = "WRITE"
|
|
489
|
+
) -> dict:
|
|
490
|
+
"""Grant a service principal READ/WRITE on a session store.
|
|
491
|
+
|
|
492
|
+
The store service performs the underlying Lakebase role provisioning and GRANTs itself, so
|
|
493
|
+
this succeeds without the caller owning the store or holding MANAGE on its Lakebase.
|
|
494
|
+
"""
|
|
495
|
+
return self._do(
|
|
496
|
+
"POST",
|
|
497
|
+
f"{_BASE}/{session_store_path(name)}/permissions:grant",
|
|
498
|
+
body={
|
|
499
|
+
"principal": {"type": "SERVICE_PRINCIPAL", "name": principal_client_id},
|
|
500
|
+
"permission": permission,
|
|
501
|
+
},
|
|
502
|
+
safe_to_retry=True,
|
|
503
|
+
)
|
|
504
|
+
|
|
505
|
+
def grant_memory_store_permission(
|
|
506
|
+
self, name: str, principal_client_id: str, permission: str = "WRITE"
|
|
507
|
+
) -> dict:
|
|
508
|
+
"""Grant a service principal READ/WRITE on a memory store (``name`` is its resource id)."""
|
|
509
|
+
return self._do(
|
|
510
|
+
"POST",
|
|
511
|
+
f"{_BASE}/{memory_store_path(name)}/permissions:grant",
|
|
512
|
+
body={
|
|
513
|
+
"principal": {"type": "SERVICE_PRINCIPAL", "name": principal_client_id},
|
|
514
|
+
"permission": permission,
|
|
515
|
+
},
|
|
516
|
+
safe_to_retry=True,
|
|
517
|
+
)
|
|
518
|
+
|
|
519
|
+
# --- sessions ------------------------------------------------------------
|
|
520
|
+
|
|
521
|
+
def create_session(
|
|
522
|
+
self,
|
|
523
|
+
store: str,
|
|
524
|
+
actor_id: str,
|
|
525
|
+
session_id: Optional[str] = None,
|
|
526
|
+
parent_session_id: Optional[str] = None,
|
|
527
|
+
metadata: Optional[dict] = None,
|
|
528
|
+
) -> models.Session:
|
|
529
|
+
body = _body(actor_id=actor_id, parent_session_id=parent_session_id, metadata=metadata)
|
|
530
|
+
return _as(
|
|
531
|
+
models.Session,
|
|
532
|
+
self._do(
|
|
533
|
+
"POST",
|
|
534
|
+
f"{_BASE}/session-stores/{store}/sessions",
|
|
535
|
+
query=_query(session_id=session_id),
|
|
536
|
+
body=body,
|
|
537
|
+
),
|
|
538
|
+
)
|
|
539
|
+
|
|
540
|
+
def list_sessions(
|
|
541
|
+
self,
|
|
542
|
+
store: str,
|
|
543
|
+
filter: Optional[str] = None,
|
|
544
|
+
order_by: Optional[str] = None,
|
|
545
|
+
page_size: Optional[int] = None,
|
|
546
|
+
page_token: Optional[str] = None,
|
|
547
|
+
) -> models.SessionList:
|
|
548
|
+
return _as(
|
|
549
|
+
models.SessionList,
|
|
550
|
+
self._do(
|
|
551
|
+
"GET",
|
|
552
|
+
f"{_BASE}/session-stores/{store}/sessions",
|
|
553
|
+
query=_query(
|
|
554
|
+
filter=filter, order_by=order_by, page_size=page_size, page_token=page_token
|
|
555
|
+
),
|
|
556
|
+
),
|
|
557
|
+
)
|
|
558
|
+
|
|
559
|
+
def get_session(self, session_id: str, store: str) -> models.Session:
|
|
560
|
+
path = f"{_BASE}/session-stores/{store}/sessions/{session_id}"
|
|
561
|
+
return _as(models.Session, self._do("GET", path))
|
|
562
|
+
|
|
563
|
+
def update_session(self, store: str, session_id: str, metadata: dict) -> models.Session:
|
|
564
|
+
return _as(
|
|
565
|
+
models.Session,
|
|
566
|
+
self._do(
|
|
567
|
+
"PATCH",
|
|
568
|
+
f"{_BASE}/session-stores/{store}/sessions/{session_id}",
|
|
569
|
+
query={"update_mask": "metadata"},
|
|
570
|
+
body=_body(metadata=metadata),
|
|
571
|
+
),
|
|
572
|
+
)
|
|
573
|
+
|
|
574
|
+
def delete_session(self, store: str, session_id: str, force: bool = False) -> dict:
|
|
575
|
+
return self._do(
|
|
576
|
+
"DELETE",
|
|
577
|
+
f"{_BASE}/session-stores/{store}/sessions/{session_id}",
|
|
578
|
+
query={"force": True} if force else None,
|
|
579
|
+
)
|
|
580
|
+
|
|
581
|
+
def fork_session(
|
|
582
|
+
self,
|
|
583
|
+
store: str,
|
|
584
|
+
source_session_id: str,
|
|
585
|
+
actor_id: str,
|
|
586
|
+
up_to_item_id: Optional[str] = None,
|
|
587
|
+
session_id: Optional[str] = None,
|
|
588
|
+
metadata: Optional[dict] = None,
|
|
589
|
+
) -> models.Session:
|
|
590
|
+
body = _body(
|
|
591
|
+
source_session_id=source_session_id,
|
|
592
|
+
actor_id=actor_id,
|
|
593
|
+
up_to_item_id=up_to_item_id,
|
|
594
|
+
session_id=session_id,
|
|
595
|
+
metadata=metadata,
|
|
596
|
+
)
|
|
597
|
+
return _as(
|
|
598
|
+
models.Session,
|
|
599
|
+
self._do("POST", f"{_BASE}/session-stores/{store}/sessions:fork", body=body),
|
|
600
|
+
)
|
|
601
|
+
|
|
602
|
+
# --- session items -------------------------------------------------------
|
|
603
|
+
|
|
604
|
+
def list_session_items(
|
|
605
|
+
self,
|
|
606
|
+
store: str,
|
|
607
|
+
session_id: str,
|
|
608
|
+
order_by: Optional[str] = None,
|
|
609
|
+
page_size: Optional[int] = None,
|
|
610
|
+
page_token: Optional[str] = None,
|
|
611
|
+
) -> models.SessionItemList:
|
|
612
|
+
return _as(
|
|
613
|
+
models.SessionItemList,
|
|
614
|
+
self._do(
|
|
615
|
+
"GET",
|
|
616
|
+
f"{_BASE}/session-stores/{store}/sessions/{session_id}/items",
|
|
617
|
+
query=_query(order_by=order_by, page_size=page_size, page_token=page_token),
|
|
618
|
+
),
|
|
619
|
+
)
|
|
620
|
+
|
|
621
|
+
def append_session_items(
|
|
622
|
+
self, store: str, session_id: str, items: list[dict]
|
|
623
|
+
) -> models.SessionItemList:
|
|
624
|
+
body = {"items": [{"data": item} for item in items]}
|
|
625
|
+
return _as(
|
|
626
|
+
models.SessionItemList,
|
|
627
|
+
self._do(
|
|
628
|
+
"POST",
|
|
629
|
+
f"{_BASE}/session-stores/{store}/sessions/{session_id}/items:append",
|
|
630
|
+
body=body,
|
|
631
|
+
),
|
|
632
|
+
)
|
|
633
|
+
|
|
634
|
+
def pop_session_item(self, store: str, session_id: str) -> models.PoppedSessionItem:
|
|
635
|
+
return _as(
|
|
636
|
+
models.PoppedSessionItem,
|
|
637
|
+
self._do(
|
|
638
|
+
"POST", f"{_BASE}/session-stores/{store}/sessions/{session_id}/items:pop", body={}
|
|
639
|
+
),
|
|
640
|
+
)
|
|
641
|
+
|
|
642
|
+
def clear_session_items(self, store: str, session_id: str) -> dict:
|
|
643
|
+
return self._do(
|
|
644
|
+
"POST", f"{_BASE}/session-stores/{store}/sessions/{session_id}/items:clear", body={}
|
|
645
|
+
)
|
|
646
|
+
|
|
647
|
+
def extract_memories(
|
|
648
|
+
self,
|
|
649
|
+
store: str,
|
|
650
|
+
session_id: str,
|
|
651
|
+
memory_store: str,
|
|
652
|
+
*,
|
|
653
|
+
instructions: Optional[str] = None,
|
|
654
|
+
dry_run: bool = False,
|
|
655
|
+
) -> models.ExtractMemoriesResponse:
|
|
656
|
+
"""Synchronously distill a session's transcript into memory entries.
|
|
657
|
+
|
|
658
|
+
Writes the entries to `memory_store` and returns them; pass `dry_run=True` to return the
|
|
659
|
+
extracted entries without persisting them.
|
|
660
|
+
"""
|
|
661
|
+
body = _body(
|
|
662
|
+
memory_store=memory_store_path(memory_store),
|
|
663
|
+
instructions=instructions,
|
|
664
|
+
dry_run=dry_run or None,
|
|
665
|
+
)
|
|
666
|
+
return _as(
|
|
667
|
+
models.ExtractMemoriesResponse,
|
|
668
|
+
self._do(
|
|
669
|
+
"POST",
|
|
670
|
+
f"{_BASE}/session-stores/{store}/sessions/{session_id}/extractions",
|
|
671
|
+
body=body,
|
|
672
|
+
),
|
|
673
|
+
)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Shared pagination validation for typed Agent Bricks resources."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
_MAX_PAGE_SIZE = 100
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _validate_bound(value: Optional[int], name: str) -> None:
|
|
9
|
+
if value is None:
|
|
10
|
+
return
|
|
11
|
+
# Reject non-integers (and bool, which is an int subclass) up front: without this a str
|
|
12
|
+
# raised a cryptic ``'<=' not supported between 'int' and 'str'`` TypeError, and a bool/float
|
|
13
|
+
# slipped through the range check (True == 1, 1.5 in [1, 100]) to reach the API as garbage.
|
|
14
|
+
if not isinstance(value, int) or isinstance(value, bool):
|
|
15
|
+
raise ValueError(f"{name} must be an integer between 1 and {_MAX_PAGE_SIZE}")
|
|
16
|
+
if not 1 <= value <= _MAX_PAGE_SIZE:
|
|
17
|
+
raise ValueError(f"{name} must be between 1 and {_MAX_PAGE_SIZE}")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def validate_page_size(page_size: Optional[int]) -> None:
|
|
21
|
+
_validate_bound(page_size, "page_size")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def validate_limit(limit: Optional[int]) -> None:
|
|
25
|
+
_validate_bound(limit, "limit")
|