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,90 @@
|
|
|
1
|
+
"""Discover the Unity Catalog AI Gateway chat model services available in a workspace.
|
|
2
|
+
|
|
3
|
+
The AI Gateway exposes Databricks-managed models as Unity Catalog *model services* in the
|
|
4
|
+
``system.ai`` schema (e.g. ``system.ai.claude-sonnet-4-5``), queryable through an OpenAI-compatible
|
|
5
|
+
endpoint at ``<host>/ai-gateway/mlflow/v1``. This lists the chat-capable ones so the demo UI's model
|
|
6
|
+
picker can offer them; the agent then calls the chosen one with ``use_ai_gateway=True``.
|
|
7
|
+
|
|
8
|
+
Kept framework-neutral (no agent SDK, no ``mlflow``) so it sits with the other neutral runtime
|
|
9
|
+
helpers and can be listed from either template's ``runtime/ui.py``.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import time
|
|
15
|
+
from typing import TYPE_CHECKING, Any, cast
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from databricks.sdk import WorkspaceClient
|
|
19
|
+
|
|
20
|
+
# The Unity Catalog REST route that lists model services under a schema.
|
|
21
|
+
_MODEL_SERVICES_PATH = "/api/2.1/unity-catalog/model-services"
|
|
22
|
+
# The AI Gateway's Databricks-managed models live in this UC schema.
|
|
23
|
+
_SYSTEM_AI_SCHEMA = "system.ai"
|
|
24
|
+
# The list API rejects page_size above 100 (InvalidParameterValue), so use its maximum.
|
|
25
|
+
_PAGE_SIZE = 100
|
|
26
|
+
# Resource-name prefix the API returns, e.g. "model-services/system.ai.claude-sonnet-4-5".
|
|
27
|
+
_NAME_PREFIX = "model-services/"
|
|
28
|
+
# The list call can intermittently fail; retry a few times so a transient blip doesn't blank the
|
|
29
|
+
# picker. A non-transient error (e.g. no permission) simply fails every attempt, then surfaces.
|
|
30
|
+
_LIST_ATTEMPTS = 3
|
|
31
|
+
_RETRY_DELAY_S = 0.5
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _is_chat_capable(supported_api_types: Any) -> bool:
|
|
35
|
+
"""True if a model service speaks a chat/completions API (i.e. not embeddings-only).
|
|
36
|
+
|
|
37
|
+
``supported_api_types`` looks like ``["openai/v1/chat/completions"]``. Be lenient when it's
|
|
38
|
+
absent (offer the model), but drop services that only advertise embeddings.
|
|
39
|
+
"""
|
|
40
|
+
types = [str(t).lower() for t in (supported_api_types or [])]
|
|
41
|
+
if not types:
|
|
42
|
+
return True
|
|
43
|
+
return any(("chat" in t or "completions" in t or "responses" in t) for t in types)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _list_page(client: WorkspaceClient, query: dict[str, Any]) -> Any:
|
|
47
|
+
"""Fetch one page of the model-services list, retrying transient failures."""
|
|
48
|
+
last_error: Exception | None = None
|
|
49
|
+
for attempt in range(_LIST_ATTEMPTS):
|
|
50
|
+
try:
|
|
51
|
+
return client.api_client.do("GET", _MODEL_SERVICES_PATH, query=query)
|
|
52
|
+
except Exception as exc: # noqa: BLE001 - retry any list failure, then surface the last one
|
|
53
|
+
last_error = exc
|
|
54
|
+
if attempt < _LIST_ATTEMPTS - 1:
|
|
55
|
+
time.sleep(_RETRY_DELAY_S)
|
|
56
|
+
raise last_error if last_error else RuntimeError("model-services list returned nothing")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def list_ai_gateway_model_services(client: WorkspaceClient) -> list[str]:
|
|
60
|
+
"""Return the names of chat-capable ``system.ai`` AI Gateway model services, sorted.
|
|
61
|
+
|
|
62
|
+
Each name is a Unity Catalog model-service path like ``system.ai.claude-sonnet-4-5`` — exactly
|
|
63
|
+
the string the OpenAI-compatible gateway (``use_ai_gateway=True``) expects as its ``model``.
|
|
64
|
+
Pages through the model-services list API, retrying transient list failures per page.
|
|
65
|
+
|
|
66
|
+
Raises whatever the underlying request raises after retries (e.g. a permission error); callers
|
|
67
|
+
that want a graceful fallback to just the agent's default model should catch it.
|
|
68
|
+
"""
|
|
69
|
+
names: list[str] = []
|
|
70
|
+
page_token: str | None = None
|
|
71
|
+
while True:
|
|
72
|
+
query: dict[str, Any] = {"parent": f"schemas/{_SYSTEM_AI_SCHEMA}", "page_size": _PAGE_SIZE}
|
|
73
|
+
if page_token:
|
|
74
|
+
query["page_token"] = page_token
|
|
75
|
+
raw = _list_page(client, query)
|
|
76
|
+
if not isinstance(raw, dict):
|
|
77
|
+
break
|
|
78
|
+
response = cast("dict[str, Any]", raw)
|
|
79
|
+
for service in response.get("model_services") or []:
|
|
80
|
+
if not isinstance(service, dict):
|
|
81
|
+
continue
|
|
82
|
+
name = str(service.get("name") or "")
|
|
83
|
+
if name.startswith(_NAME_PREFIX):
|
|
84
|
+
name = name[len(_NAME_PREFIX) :]
|
|
85
|
+
if name and _is_chat_capable(service.get("supported_api_types")):
|
|
86
|
+
names.append(name)
|
|
87
|
+
page_token = response.get("next_page_token") or None
|
|
88
|
+
if not page_token:
|
|
89
|
+
break
|
|
90
|
+
return sorted(names)
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
"""Transport-neutral facade for Agent Bricks invocation execution."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import copy
|
|
7
|
+
from collections.abc import Callable
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
from databricks_agentkit.runtime.execution import (
|
|
11
|
+
AttemptExecution,
|
|
12
|
+
InvocationExecutor,
|
|
13
|
+
LocalInvocationExecutor,
|
|
14
|
+
copy_json_value,
|
|
15
|
+
)
|
|
16
|
+
from databricks_agentkit.runtime.store import (
|
|
17
|
+
InMemoryRuntimeStore,
|
|
18
|
+
RuntimeStore,
|
|
19
|
+
runtime_store_from_environment,
|
|
20
|
+
)
|
|
21
|
+
from databricks_agentkit.runtime.types import (
|
|
22
|
+
Invocation,
|
|
23
|
+
InvocationEvent,
|
|
24
|
+
InvocationExecutorFn,
|
|
25
|
+
InvocationFailedError,
|
|
26
|
+
InvocationNotFoundError,
|
|
27
|
+
InvocationStatus,
|
|
28
|
+
JsonValue,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
if TYPE_CHECKING:
|
|
32
|
+
from databricks_agentkit.runtime.durability.store import DurableRuntimeStore
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class Runtime:
|
|
36
|
+
"""Coordinate one Agent Bricks process's invocation lifecycle.
|
|
37
|
+
|
|
38
|
+
A Runtime Store owns invocation state: requests, status, events, and results. An invocation
|
|
39
|
+
executor turns queued state into agent attempts. This facade accepts idempotent requests,
|
|
40
|
+
delegates scheduling to the executor, and polls the store for foreground results or event replay.
|
|
41
|
+
Use :meth:`local` for process-local execution, :meth:`durable` for a durable store,
|
|
42
|
+
:meth:`from_store` for an explicit store, or :meth:`from_environment` when Agent Bricks configures the
|
|
43
|
+
store through the process environment.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
def __init__(
|
|
47
|
+
self,
|
|
48
|
+
*,
|
|
49
|
+
runtime_store: RuntimeStore,
|
|
50
|
+
executor: InvocationExecutor,
|
|
51
|
+
poll_seconds: float = 1.0,
|
|
52
|
+
) -> None:
|
|
53
|
+
if poll_seconds <= 0:
|
|
54
|
+
raise ValueError("poll_seconds must be positive")
|
|
55
|
+
self.runtime_store = runtime_store
|
|
56
|
+
self.executor = executor
|
|
57
|
+
self.poll_seconds = poll_seconds
|
|
58
|
+
self._started = False
|
|
59
|
+
|
|
60
|
+
@classmethod
|
|
61
|
+
def local(
|
|
62
|
+
cls,
|
|
63
|
+
execute_fn: InvocationExecutorFn,
|
|
64
|
+
*,
|
|
65
|
+
runtime_store: RuntimeStore | None = None,
|
|
66
|
+
poll_seconds: float = 1.0,
|
|
67
|
+
) -> Runtime:
|
|
68
|
+
"""Construct a Runtime that schedules work only in the current process.
|
|
69
|
+
|
|
70
|
+
The default :class:`InMemoryRuntimeStore` supports foreground, background, streaming,
|
|
71
|
+
polling, and event replay until the process exits. It does not heartbeat or recover work.
|
|
72
|
+
"""
|
|
73
|
+
store = InMemoryRuntimeStore() if runtime_store is None else runtime_store
|
|
74
|
+
from databricks_agentkit.runtime.durability.store import DurableRuntimeStore
|
|
75
|
+
|
|
76
|
+
if isinstance(store, DurableRuntimeStore):
|
|
77
|
+
raise TypeError("Runtime.local requires a non-durable RuntimeStore")
|
|
78
|
+
execution = AttemptExecution(execute_fn, runtime_store=store)
|
|
79
|
+
return cls(
|
|
80
|
+
runtime_store=store,
|
|
81
|
+
executor=LocalInvocationExecutor(execution, runtime_store=store),
|
|
82
|
+
poll_seconds=poll_seconds,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
@classmethod
|
|
86
|
+
def durable(
|
|
87
|
+
cls,
|
|
88
|
+
execute_fn: InvocationExecutorFn,
|
|
89
|
+
*,
|
|
90
|
+
runtime_store: DurableRuntimeStore,
|
|
91
|
+
recovery_enabled: Callable[[], bool] | None = None,
|
|
92
|
+
heartbeat_seconds: float = 3.0,
|
|
93
|
+
stale_seconds: float = 10.0,
|
|
94
|
+
scan_seconds: float = 3.0,
|
|
95
|
+
poll_seconds: float = 1.0,
|
|
96
|
+
) -> Runtime:
|
|
97
|
+
"""Construct a Runtime that heartbeats and can recover work from a durable store.
|
|
98
|
+
|
|
99
|
+
A durable runtime requires a :class:`DurableRuntimeStore`. Its results and events persist
|
|
100
|
+
across process restarts. ``recovery_enabled`` controls only stale-work recovery; it does not
|
|
101
|
+
change whether normal invocation state is persisted.
|
|
102
|
+
"""
|
|
103
|
+
from databricks_agentkit.runtime.durability.execution import DurableInvocationExecutor
|
|
104
|
+
from databricks_agentkit.runtime.durability.store import DurableRuntimeStore
|
|
105
|
+
|
|
106
|
+
if not isinstance(runtime_store, DurableRuntimeStore):
|
|
107
|
+
raise TypeError("Runtime.durable requires a DurableRuntimeStore")
|
|
108
|
+
execution = AttemptExecution(execute_fn, runtime_store=runtime_store)
|
|
109
|
+
return cls(
|
|
110
|
+
runtime_store=runtime_store,
|
|
111
|
+
executor=DurableInvocationExecutor(
|
|
112
|
+
execution,
|
|
113
|
+
runtime_store=runtime_store,
|
|
114
|
+
recovery_enabled=recovery_enabled or (lambda: True),
|
|
115
|
+
heartbeat_seconds=heartbeat_seconds,
|
|
116
|
+
stale_seconds=stale_seconds,
|
|
117
|
+
scan_seconds=scan_seconds,
|
|
118
|
+
),
|
|
119
|
+
poll_seconds=poll_seconds,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
@classmethod
|
|
123
|
+
def from_store(
|
|
124
|
+
cls,
|
|
125
|
+
execute_fn: InvocationExecutorFn,
|
|
126
|
+
*,
|
|
127
|
+
runtime_store: RuntimeStore,
|
|
128
|
+
recovery_enabled: Callable[[], bool] | None = None,
|
|
129
|
+
heartbeat_seconds: float = 3.0,
|
|
130
|
+
stale_seconds: float = 10.0,
|
|
131
|
+
scan_seconds: float = 3.0,
|
|
132
|
+
poll_seconds: float = 1.0,
|
|
133
|
+
) -> Runtime:
|
|
134
|
+
"""Construct the local or durable Runtime required by the selected Runtime Store."""
|
|
135
|
+
from databricks_agentkit.runtime.durability.store import DurableRuntimeStore
|
|
136
|
+
|
|
137
|
+
if isinstance(runtime_store, DurableRuntimeStore):
|
|
138
|
+
return cls.durable(
|
|
139
|
+
execute_fn,
|
|
140
|
+
runtime_store=runtime_store,
|
|
141
|
+
recovery_enabled=recovery_enabled,
|
|
142
|
+
heartbeat_seconds=heartbeat_seconds,
|
|
143
|
+
stale_seconds=stale_seconds,
|
|
144
|
+
scan_seconds=scan_seconds,
|
|
145
|
+
poll_seconds=poll_seconds,
|
|
146
|
+
)
|
|
147
|
+
return cls.local(execute_fn, runtime_store=runtime_store, poll_seconds=poll_seconds)
|
|
148
|
+
|
|
149
|
+
@classmethod
|
|
150
|
+
def from_environment(
|
|
151
|
+
cls,
|
|
152
|
+
execute_fn: InvocationExecutorFn,
|
|
153
|
+
*,
|
|
154
|
+
recovery_enabled: Callable[[], bool] | None = None,
|
|
155
|
+
heartbeat_seconds: float = 3.0,
|
|
156
|
+
stale_seconds: float = 10.0,
|
|
157
|
+
scan_seconds: float = 3.0,
|
|
158
|
+
poll_seconds: float = 1.0,
|
|
159
|
+
) -> Runtime:
|
|
160
|
+
"""Construct the Runtime selected by the Agent Bricks process environment."""
|
|
161
|
+
return cls.from_store(
|
|
162
|
+
execute_fn,
|
|
163
|
+
runtime_store=runtime_store_from_environment(),
|
|
164
|
+
recovery_enabled=recovery_enabled,
|
|
165
|
+
heartbeat_seconds=heartbeat_seconds,
|
|
166
|
+
stale_seconds=stale_seconds,
|
|
167
|
+
scan_seconds=scan_seconds,
|
|
168
|
+
poll_seconds=poll_seconds,
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
@property
|
|
172
|
+
def is_durable(self) -> bool:
|
|
173
|
+
"""Whether this Runtime has heartbeat and recovery capabilities."""
|
|
174
|
+
from databricks_agentkit.runtime.durability.store import DurableRuntimeStore
|
|
175
|
+
|
|
176
|
+
return isinstance(self.runtime_store, DurableRuntimeStore)
|
|
177
|
+
|
|
178
|
+
async def start(self) -> None:
|
|
179
|
+
"""Initialize the Runtime Store and invocation executor."""
|
|
180
|
+
if self._started:
|
|
181
|
+
return
|
|
182
|
+
await self.runtime_store.initialize()
|
|
183
|
+
try:
|
|
184
|
+
await self.executor.start()
|
|
185
|
+
except Exception:
|
|
186
|
+
await self.runtime_store.close()
|
|
187
|
+
raise
|
|
188
|
+
self._started = True
|
|
189
|
+
|
|
190
|
+
async def stop(self) -> None:
|
|
191
|
+
"""Stop local work and close the Runtime Store."""
|
|
192
|
+
if not self._started:
|
|
193
|
+
return
|
|
194
|
+
await self.executor.stop()
|
|
195
|
+
self._started = False
|
|
196
|
+
await self.runtime_store.close()
|
|
197
|
+
|
|
198
|
+
async def submit(self, invocation_id: str, request: JsonValue) -> Invocation:
|
|
199
|
+
"""Accept an idempotent invocation and schedule it on this worker.
|
|
200
|
+
|
|
201
|
+
The Runtime Store makes ``invocation_id`` the idempotency key. Retrying the same request
|
|
202
|
+
returns its existing state; a different request for the same ID raises
|
|
203
|
+
:class:`InvocationConflictError`.
|
|
204
|
+
"""
|
|
205
|
+
self._require_started()
|
|
206
|
+
if not invocation_id:
|
|
207
|
+
raise ValueError("invocation_id must not be empty")
|
|
208
|
+
state = await self.runtime_store.accept(
|
|
209
|
+
invocation_id,
|
|
210
|
+
copy_json_value(request, "request"),
|
|
211
|
+
)
|
|
212
|
+
self.executor.ensure_scheduled(state)
|
|
213
|
+
return state
|
|
214
|
+
|
|
215
|
+
async def invoke(
|
|
216
|
+
self,
|
|
217
|
+
invocation_id: str,
|
|
218
|
+
request: JsonValue,
|
|
219
|
+
*,
|
|
220
|
+
timeout: float | None = None,
|
|
221
|
+
) -> JsonValue:
|
|
222
|
+
"""Accept an invocation and wait for its terminal result."""
|
|
223
|
+
await self.submit(invocation_id, request)
|
|
224
|
+
return await self.wait(invocation_id, timeout=timeout)
|
|
225
|
+
|
|
226
|
+
async def get_invocation(self, invocation_id: str) -> Invocation | None:
|
|
227
|
+
"""Return current state and ensure queued first-attempt work is scheduled locally."""
|
|
228
|
+
self._require_started()
|
|
229
|
+
state = await self.runtime_store.get(invocation_id)
|
|
230
|
+
if state is not None:
|
|
231
|
+
self.executor.ensure_scheduled(state)
|
|
232
|
+
return state
|
|
233
|
+
|
|
234
|
+
async def wait(
|
|
235
|
+
self,
|
|
236
|
+
invocation_id: str,
|
|
237
|
+
*,
|
|
238
|
+
timeout: float | None = None,
|
|
239
|
+
) -> JsonValue:
|
|
240
|
+
"""Poll until an invocation completes, fails, or reaches the optional timeout."""
|
|
241
|
+
self._require_started()
|
|
242
|
+
|
|
243
|
+
async def poll() -> JsonValue:
|
|
244
|
+
while True:
|
|
245
|
+
state = await self.get_invocation(invocation_id)
|
|
246
|
+
if state is None:
|
|
247
|
+
raise InvocationNotFoundError(invocation_id)
|
|
248
|
+
if state.status == InvocationStatus.COMPLETED:
|
|
249
|
+
return copy.deepcopy(state.response)
|
|
250
|
+
if state.status == InvocationStatus.FAILED:
|
|
251
|
+
raise InvocationFailedError(invocation_id)
|
|
252
|
+
await asyncio.sleep(self.poll_seconds)
|
|
253
|
+
|
|
254
|
+
if timeout is None:
|
|
255
|
+
return await poll()
|
|
256
|
+
try:
|
|
257
|
+
return await asyncio.wait_for(poll(), timeout=timeout)
|
|
258
|
+
except asyncio.TimeoutError as exc:
|
|
259
|
+
raise TimeoutError from exc
|
|
260
|
+
|
|
261
|
+
async def get_events(
|
|
262
|
+
self,
|
|
263
|
+
invocation_id: str,
|
|
264
|
+
*,
|
|
265
|
+
after_sequence: int | None = None,
|
|
266
|
+
) -> list[InvocationEvent]:
|
|
267
|
+
"""Return persisted invocation events after an optional exclusive replay cursor."""
|
|
268
|
+
self._require_started()
|
|
269
|
+
return await self.runtime_store.events(invocation_id, after_sequence)
|
|
270
|
+
|
|
271
|
+
def _require_started(self) -> None:
|
|
272
|
+
if not self._started:
|
|
273
|
+
raise RuntimeError("Runtime.start() must be called first")
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""Minimal Databricks Session Store REST client (vendored).
|
|
2
|
+
|
|
3
|
+
Wraps the managed Session Store API (``/api/2.0/agents/session-stores``) — the SDK-agnostic durable
|
|
4
|
+
store for agent session history — with just the calls the LangGraph saver needs: resolve a store,
|
|
5
|
+
get-or-create a session, and append / list / clear its ordered items. Auth/host come from a
|
|
6
|
+
``databricks.sdk.WorkspaceClient`` (default credentials).
|
|
7
|
+
|
|
8
|
+
This is a trimmed stand-in for the first-party ``databricks_agent_client.SessionStoreClient``; when
|
|
9
|
+
that package is published, import it instead and delete this file. Item ``data`` is opaque JSON, so
|
|
10
|
+
callers store whatever shape they like (the saver stores checkpoint fragments).
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from dataclasses import dataclass, field
|
|
16
|
+
from typing import Any, Iterator, Optional, Sequence
|
|
17
|
+
|
|
18
|
+
from databricks.sdk import WorkspaceClient
|
|
19
|
+
|
|
20
|
+
from databricks_agentkit.runtime.workspace import workspace_client as default_workspace_client
|
|
21
|
+
|
|
22
|
+
_API_ROOT = "/api/2.0/agents"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(frozen=True)
|
|
26
|
+
class Session:
|
|
27
|
+
"""A durable interaction within a Session Store, addressed by (store, session_id)."""
|
|
28
|
+
|
|
29
|
+
session_store_name: str
|
|
30
|
+
session_id: str
|
|
31
|
+
actor_id: str
|
|
32
|
+
metadata: dict[str, str] = field(default_factory=dict)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass(frozen=True)
|
|
36
|
+
class SessionItem:
|
|
37
|
+
"""One opaque, ordered history entry. ``data`` is arbitrary JSON."""
|
|
38
|
+
|
|
39
|
+
item_id: str
|
|
40
|
+
data: Any
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class SessionStoreClient:
|
|
44
|
+
"""Thin REST client over the managed Session Store API."""
|
|
45
|
+
|
|
46
|
+
def __init__(self, workspace_client: Optional[WorkspaceClient] = None) -> None:
|
|
47
|
+
self._api = (workspace_client or default_workspace_client()).api_client
|
|
48
|
+
self._store_name: Optional[str] = None
|
|
49
|
+
|
|
50
|
+
def set_session_store(self, session_store_name: str) -> "SessionStoreClient":
|
|
51
|
+
"""Bind this client to a store; returns self so it doubles as the store handle."""
|
|
52
|
+
if not session_store_name:
|
|
53
|
+
raise ValueError("session_store_name is required")
|
|
54
|
+
self._store_name = session_store_name
|
|
55
|
+
return self
|
|
56
|
+
|
|
57
|
+
def get_session(self, *, session_id: str) -> Session:
|
|
58
|
+
resp: dict[str, Any] = self._api.do("GET", f"{self._sessions_path()}/{session_id}") # type: ignore[assignment]
|
|
59
|
+
return self._session(resp)
|
|
60
|
+
|
|
61
|
+
def create_session(
|
|
62
|
+
self,
|
|
63
|
+
*,
|
|
64
|
+
actor_id: str,
|
|
65
|
+
session_id: Optional[str] = None,
|
|
66
|
+
metadata: Optional[dict[str, str]] = None,
|
|
67
|
+
) -> Session:
|
|
68
|
+
body: dict[str, Any] = {"actor_id": actor_id}
|
|
69
|
+
if metadata:
|
|
70
|
+
body["metadata"] = metadata
|
|
71
|
+
query = {"session_id": session_id} if session_id else None
|
|
72
|
+
resp: dict[str, Any] = self._api.do("POST", self._sessions_path(), query=query, body=body) # type: ignore[assignment]
|
|
73
|
+
return self._session(resp)
|
|
74
|
+
|
|
75
|
+
def append_items(self, session: Session, *, items: Sequence[Any]) -> None:
|
|
76
|
+
if not items:
|
|
77
|
+
raise ValueError("at least one item is required")
|
|
78
|
+
self._api.do(
|
|
79
|
+
"POST",
|
|
80
|
+
f"{self._items_path(session)}:append",
|
|
81
|
+
body={"items": [{"data": item} for item in items]},
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
def list_items(
|
|
85
|
+
self, session: Session, *, order_by: Optional[str] = None
|
|
86
|
+
) -> Iterator[SessionItem]:
|
|
87
|
+
"""Yield the session's items (paginating transparently)."""
|
|
88
|
+
page_token: Optional[str] = None
|
|
89
|
+
while True:
|
|
90
|
+
query = {k: v for k, v in {"order_by": order_by, "page_token": page_token}.items() if v}
|
|
91
|
+
resp: dict[str, Any] = self._api.do(
|
|
92
|
+
"GET", self._items_path(session), query=query or None
|
|
93
|
+
) # type: ignore[assignment]
|
|
94
|
+
for item in resp.get("session_items", []):
|
|
95
|
+
if "data" in item:
|
|
96
|
+
yield SessionItem(item_id=item.get("item_id", ""), data=item["data"])
|
|
97
|
+
page_token = resp.get("next_page_token")
|
|
98
|
+
if not page_token:
|
|
99
|
+
return
|
|
100
|
+
|
|
101
|
+
def clear_items(self, session: Session) -> None:
|
|
102
|
+
self._api.do("POST", f"{self._items_path(session)}:clear", body={})
|
|
103
|
+
|
|
104
|
+
# ----- internals ----------------------------------------------------------
|
|
105
|
+
|
|
106
|
+
def _sessions_path(self) -> str:
|
|
107
|
+
if not self._store_name:
|
|
108
|
+
raise ValueError("call set_session_store() before using the client")
|
|
109
|
+
return f"{_API_ROOT}/session-stores/{self._store_name}/sessions"
|
|
110
|
+
|
|
111
|
+
def _items_path(self, session: Session) -> str:
|
|
112
|
+
return f"{self._sessions_path()}/{session.session_id}/items"
|
|
113
|
+
|
|
114
|
+
def _session(self, resp: dict[str, Any]) -> Session:
|
|
115
|
+
return Session(
|
|
116
|
+
session_store_name=resp.get("session_store_name", self._store_name or ""),
|
|
117
|
+
session_id=resp["session_id"],
|
|
118
|
+
actor_id=resp.get("actor_id", ""),
|
|
119
|
+
metadata=dict(resp.get("metadata", {})),
|
|
120
|
+
)
|