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,287 @@
|
|
|
1
|
+
"""Runtime Store contracts and process-local implementation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import copy
|
|
7
|
+
import os
|
|
8
|
+
from typing import Protocol
|
|
9
|
+
|
|
10
|
+
from databricks_agentkit.runtime.types import (
|
|
11
|
+
Invocation,
|
|
12
|
+
InvocationConflictError,
|
|
13
|
+
InvocationEvent,
|
|
14
|
+
InvocationStatus,
|
|
15
|
+
JsonObject,
|
|
16
|
+
JsonValue,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
DEFAULT_RUNTIME_STORE_SCHEMA = "databricks_agentkit_runtime"
|
|
20
|
+
RUNTIME_STORE_LAKEBASE_BRANCH_ENV = "DATABRICKS_AGENTBRICKS_RUNTIME_STORE_LAKEBASE_BRANCH"
|
|
21
|
+
RUNTIME_STORE_LAKEBASE_ENDPOINT_ENV = "DATABRICKS_AGENTBRICKS_RUNTIME_STORE_LAKEBASE_ENDPOINT"
|
|
22
|
+
RUNTIME_STORE_DATABASE_ENV = "DATABRICKS_AGENTBRICKS_RUNTIME_STORE_DATABASE"
|
|
23
|
+
RUNTIME_STORE_USERNAME_ENV = "DATABRICKS_AGENTBRICKS_RUNTIME_STORE_USERNAME"
|
|
24
|
+
RUNTIME_STORE_SCHEMA_ENV = "DATABRICKS_AGENTBRICKS_RUNTIME_STORE_SCHEMA"
|
|
25
|
+
RUNTIME_STORE_LOCAL_ENV = "DATABRICKS_AGENTBRICKS_RUNTIME_STORE_LOCAL"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _validate_invocation_id(invocation_id: str) -> None:
|
|
29
|
+
if not invocation_id:
|
|
30
|
+
raise ValueError("invocation_id must not be empty")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class RuntimeStore(Protocol):
|
|
34
|
+
"""Invocation state operations shared by local and durable Runtime Stores."""
|
|
35
|
+
|
|
36
|
+
async def initialize(self) -> None:
|
|
37
|
+
"""Open the store and create any resources it owns."""
|
|
38
|
+
...
|
|
39
|
+
|
|
40
|
+
async def close(self) -> None:
|
|
41
|
+
"""Release the store's connections and other resources."""
|
|
42
|
+
...
|
|
43
|
+
|
|
44
|
+
async def accept(self, invocation_id: str, request: JsonValue) -> Invocation:
|
|
45
|
+
"""Create an invocation or return the identical request accepted for this ID.
|
|
46
|
+
|
|
47
|
+
Reusing an invocation ID with a different request raises :class:`InvocationConflictError`.
|
|
48
|
+
"""
|
|
49
|
+
...
|
|
50
|
+
|
|
51
|
+
async def get(self, invocation_id: str) -> Invocation | None:
|
|
52
|
+
"""Return the latest invocation state, or ``None`` for an unknown ID."""
|
|
53
|
+
...
|
|
54
|
+
|
|
55
|
+
async def claim(self, invocation_id: str) -> Invocation | None:
|
|
56
|
+
"""Claim a newly queued invocation for its first attempt."""
|
|
57
|
+
...
|
|
58
|
+
|
|
59
|
+
async def complete(
|
|
60
|
+
self,
|
|
61
|
+
invocation_id: str,
|
|
62
|
+
attempt: int,
|
|
63
|
+
response: JsonValue,
|
|
64
|
+
) -> bool:
|
|
65
|
+
"""Persist a result when this attempt still owns the active invocation."""
|
|
66
|
+
...
|
|
67
|
+
|
|
68
|
+
async def fail(self, invocation_id: str, attempt: int) -> bool:
|
|
69
|
+
"""Mark an invocation failed when this attempt still owns it."""
|
|
70
|
+
...
|
|
71
|
+
|
|
72
|
+
async def append_event(
|
|
73
|
+
self,
|
|
74
|
+
invocation_id: str,
|
|
75
|
+
attempt: int,
|
|
76
|
+
event: JsonObject,
|
|
77
|
+
) -> int | None:
|
|
78
|
+
"""Append an event for an owned attempt and return its replay cursor."""
|
|
79
|
+
...
|
|
80
|
+
|
|
81
|
+
async def events(
|
|
82
|
+
self,
|
|
83
|
+
invocation_id: str,
|
|
84
|
+
after_sequence: int | None = None,
|
|
85
|
+
) -> list[InvocationEvent]:
|
|
86
|
+
"""Return invocation events after the optional exclusive replay cursor."""
|
|
87
|
+
...
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class InMemoryRuntimeStore(RuntimeStore):
|
|
91
|
+
"""Process-local Runtime Store for development and tests."""
|
|
92
|
+
|
|
93
|
+
def __init__(self) -> None:
|
|
94
|
+
self.states: dict[str, Invocation] = {}
|
|
95
|
+
self.persisted_events: list[InvocationEvent] = []
|
|
96
|
+
self._lock = asyncio.Lock()
|
|
97
|
+
|
|
98
|
+
async def initialize(self) -> None:
|
|
99
|
+
pass
|
|
100
|
+
|
|
101
|
+
async def close(self) -> None:
|
|
102
|
+
pass
|
|
103
|
+
|
|
104
|
+
async def accept(self, invocation_id: str, request: JsonValue) -> Invocation:
|
|
105
|
+
_validate_invocation_id(invocation_id)
|
|
106
|
+
async with self._lock:
|
|
107
|
+
existing = self.states.get(invocation_id)
|
|
108
|
+
if existing is not None:
|
|
109
|
+
if existing.request != request:
|
|
110
|
+
raise InvocationConflictError(invocation_id)
|
|
111
|
+
return copy.deepcopy(existing)
|
|
112
|
+
state = Invocation(
|
|
113
|
+
invocation_id=invocation_id,
|
|
114
|
+
status=InvocationStatus.QUEUED,
|
|
115
|
+
attempt=0,
|
|
116
|
+
request=copy.deepcopy(request),
|
|
117
|
+
response=None,
|
|
118
|
+
)
|
|
119
|
+
self.states[invocation_id] = state
|
|
120
|
+
return copy.deepcopy(state)
|
|
121
|
+
|
|
122
|
+
async def get(self, invocation_id: str) -> Invocation | None:
|
|
123
|
+
_validate_invocation_id(invocation_id)
|
|
124
|
+
async with self._lock:
|
|
125
|
+
state = self.states.get(invocation_id)
|
|
126
|
+
return copy.deepcopy(state) if state is not None else None
|
|
127
|
+
|
|
128
|
+
async def claim(self, invocation_id: str) -> Invocation | None:
|
|
129
|
+
_validate_invocation_id(invocation_id)
|
|
130
|
+
async with self._lock:
|
|
131
|
+
state = self.states.get(invocation_id)
|
|
132
|
+
if state is None or state.status != InvocationStatus.QUEUED:
|
|
133
|
+
return None
|
|
134
|
+
claimed = Invocation(
|
|
135
|
+
invocation_id=invocation_id,
|
|
136
|
+
status=InvocationStatus.ACTIVE,
|
|
137
|
+
attempt=state.attempt + 1,
|
|
138
|
+
request=copy.deepcopy(state.request),
|
|
139
|
+
response=None,
|
|
140
|
+
)
|
|
141
|
+
self.states[invocation_id] = claimed
|
|
142
|
+
self._append_event(invocation_id, claimed.attempt, {"type": "run.started"})
|
|
143
|
+
return copy.deepcopy(claimed)
|
|
144
|
+
|
|
145
|
+
async def complete(self, invocation_id: str, attempt: int, response: JsonValue) -> bool:
|
|
146
|
+
async with self._lock:
|
|
147
|
+
state = self.states.get(invocation_id)
|
|
148
|
+
if state is None or not self._owns_attempt(state, attempt):
|
|
149
|
+
return False
|
|
150
|
+
self.states[invocation_id] = Invocation(
|
|
151
|
+
invocation_id=state.invocation_id,
|
|
152
|
+
status=InvocationStatus.COMPLETED,
|
|
153
|
+
attempt=state.attempt,
|
|
154
|
+
request=state.request,
|
|
155
|
+
response=copy.deepcopy(response),
|
|
156
|
+
)
|
|
157
|
+
self._append_event(invocation_id, attempt, {"type": "run.completed"})
|
|
158
|
+
return True
|
|
159
|
+
|
|
160
|
+
async def fail(self, invocation_id: str, attempt: int) -> bool:
|
|
161
|
+
async with self._lock:
|
|
162
|
+
state = self.states.get(invocation_id)
|
|
163
|
+
if state is None or not self._owns_attempt(state, attempt):
|
|
164
|
+
return False
|
|
165
|
+
self.states[invocation_id] = Invocation(
|
|
166
|
+
invocation_id=state.invocation_id,
|
|
167
|
+
status=InvocationStatus.FAILED,
|
|
168
|
+
attempt=state.attempt,
|
|
169
|
+
request=state.request,
|
|
170
|
+
response=None,
|
|
171
|
+
)
|
|
172
|
+
self._append_event(invocation_id, attempt, {"type": "run.failed"})
|
|
173
|
+
return True
|
|
174
|
+
|
|
175
|
+
async def append_event(
|
|
176
|
+
self,
|
|
177
|
+
invocation_id: str,
|
|
178
|
+
attempt: int,
|
|
179
|
+
event: JsonObject,
|
|
180
|
+
) -> int | None:
|
|
181
|
+
async with self._lock:
|
|
182
|
+
state = self.states.get(invocation_id)
|
|
183
|
+
if not self._owns_attempt(state, attempt):
|
|
184
|
+
return None
|
|
185
|
+
persisted = self._append_event(invocation_id, attempt, event)
|
|
186
|
+
return persisted.sequence_number
|
|
187
|
+
|
|
188
|
+
async def events(
|
|
189
|
+
self,
|
|
190
|
+
invocation_id: str,
|
|
191
|
+
after_sequence: int | None = None,
|
|
192
|
+
) -> list[InvocationEvent]:
|
|
193
|
+
_validate_invocation_id(invocation_id)
|
|
194
|
+
async with self._lock:
|
|
195
|
+
return [
|
|
196
|
+
copy.deepcopy(event)
|
|
197
|
+
for event in self.persisted_events
|
|
198
|
+
if event.invocation_id == invocation_id
|
|
199
|
+
and (after_sequence is None or event.sequence_number > after_sequence)
|
|
200
|
+
]
|
|
201
|
+
|
|
202
|
+
@staticmethod
|
|
203
|
+
def _owns_attempt(state: Invocation | None, attempt: int) -> bool:
|
|
204
|
+
return bool(state and state.status == InvocationStatus.ACTIVE and state.attempt == attempt)
|
|
205
|
+
|
|
206
|
+
def _append_event(self, invocation_id: str, attempt: int, event: JsonObject) -> InvocationEvent:
|
|
207
|
+
persisted = InvocationEvent(
|
|
208
|
+
sequence_number=len(self.persisted_events) + 1,
|
|
209
|
+
invocation_id=invocation_id,
|
|
210
|
+
attempt=attempt,
|
|
211
|
+
event=copy.deepcopy(event),
|
|
212
|
+
)
|
|
213
|
+
self.persisted_events.append(persisted)
|
|
214
|
+
return persisted
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def runtime_store_is_persistent_environment() -> bool:
|
|
218
|
+
"""Whether the process environment selects a complete durable Runtime Store."""
|
|
219
|
+
if os.getenv(RUNTIME_STORE_LOCAL_ENV, "").lower() == "true":
|
|
220
|
+
return False
|
|
221
|
+
managed = (
|
|
222
|
+
os.getenv(RUNTIME_STORE_LAKEBASE_BRANCH_ENV),
|
|
223
|
+
os.getenv(RUNTIME_STORE_DATABASE_ENV),
|
|
224
|
+
os.getenv(RUNTIME_STORE_USERNAME_ENV),
|
|
225
|
+
)
|
|
226
|
+
if any(managed):
|
|
227
|
+
return all(managed)
|
|
228
|
+
legacy = (
|
|
229
|
+
os.getenv(RUNTIME_STORE_LAKEBASE_ENDPOINT_ENV),
|
|
230
|
+
os.getenv(RUNTIME_STORE_SCHEMA_ENV),
|
|
231
|
+
)
|
|
232
|
+
return all(legacy)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def runtime_store_from_environment() -> RuntimeStore:
|
|
236
|
+
"""Construct the Runtime Store selected by the process environment.
|
|
237
|
+
|
|
238
|
+
The local marker overrides any inherited Lakebase variables. Managed Runtime Stores provide a
|
|
239
|
+
branch, database, and username; legacy Apps resources provide an endpoint plus ``PG*`` values.
|
|
240
|
+
An incomplete durable configuration is rejected rather than mixed with another resource.
|
|
241
|
+
"""
|
|
242
|
+
if os.getenv(RUNTIME_STORE_LOCAL_ENV, "").lower() == "true":
|
|
243
|
+
return InMemoryRuntimeStore()
|
|
244
|
+
|
|
245
|
+
branch = os.getenv(RUNTIME_STORE_LAKEBASE_BRANCH_ENV)
|
|
246
|
+
database = os.getenv(RUNTIME_STORE_DATABASE_ENV)
|
|
247
|
+
username = os.getenv(RUNTIME_STORE_USERNAME_ENV)
|
|
248
|
+
managed = {
|
|
249
|
+
RUNTIME_STORE_LAKEBASE_BRANCH_ENV: branch,
|
|
250
|
+
RUNTIME_STORE_DATABASE_ENV: database,
|
|
251
|
+
RUNTIME_STORE_USERNAME_ENV: username,
|
|
252
|
+
}
|
|
253
|
+
if any(managed.values()):
|
|
254
|
+
missing = [name for name, value in managed.items() if not value]
|
|
255
|
+
if missing:
|
|
256
|
+
raise RuntimeError(
|
|
257
|
+
"Managed Runtime Store configuration is missing: " + ", ".join(missing)
|
|
258
|
+
)
|
|
259
|
+
from databricks_agentkit.runtime.durability.lakebase_runtime_store import (
|
|
260
|
+
LakebaseDurableRuntimeStore,
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
assert branch is not None
|
|
264
|
+
assert database is not None
|
|
265
|
+
assert username is not None
|
|
266
|
+
schema = os.getenv(RUNTIME_STORE_SCHEMA_ENV) or DEFAULT_RUNTIME_STORE_SCHEMA
|
|
267
|
+
return LakebaseDurableRuntimeStore.from_managed_runtime_store(
|
|
268
|
+
branch=branch,
|
|
269
|
+
database=database,
|
|
270
|
+
username=username,
|
|
271
|
+
schema=schema,
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
endpoint = os.getenv(RUNTIME_STORE_LAKEBASE_ENDPOINT_ENV)
|
|
275
|
+
schema = os.getenv(RUNTIME_STORE_SCHEMA_ENV)
|
|
276
|
+
if endpoint and schema:
|
|
277
|
+
from databricks_agentkit.runtime.durability.lakebase_runtime_store import (
|
|
278
|
+
LakebaseDurableRuntimeStore,
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
return LakebaseDurableRuntimeStore.from_app_resource(endpoint=endpoint, schema=schema)
|
|
282
|
+
if endpoint or schema:
|
|
283
|
+
raise RuntimeError(
|
|
284
|
+
f"{RUNTIME_STORE_LAKEBASE_ENDPOINT_ENV} and {RUNTIME_STORE_SCHEMA_ENV} must be set "
|
|
285
|
+
"together"
|
|
286
|
+
)
|
|
287
|
+
return InMemoryRuntimeStore()
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
"""Read the framework-neutral tool bindings in the project's ``agent.toml``."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import pathlib
|
|
7
|
+
import re
|
|
8
|
+
from collections.abc import Mapping
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from typing import Any, cast
|
|
11
|
+
|
|
12
|
+
try:
|
|
13
|
+
import tomllib # ty: ignore[unresolved-import]
|
|
14
|
+
except ModuleNotFoundError:
|
|
15
|
+
import tomli as tomllib
|
|
16
|
+
|
|
17
|
+
# The env vars that carry the managed store bindings to the deployed runtime. (The agent.toml table
|
|
18
|
+
# names live in `agent_project`, next to the manifest parsing that reads them.)
|
|
19
|
+
MEMORY_STORE_ENV = "AGENT_MEMORY_STORE"
|
|
20
|
+
SESSION_STORE_ENV = "AGENT_SESSION_STORE"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ToolManifestError(RuntimeError):
|
|
24
|
+
"""Invalid declarative tool configuration that runtime adapters must surface."""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True)
|
|
28
|
+
class ScopeRecord:
|
|
29
|
+
kind: str
|
|
30
|
+
value: str
|
|
31
|
+
permission: str
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass(frozen=True)
|
|
35
|
+
class ToolRecord:
|
|
36
|
+
id: str
|
|
37
|
+
kind: str
|
|
38
|
+
service: str | None = None
|
|
39
|
+
function: str | None = None
|
|
40
|
+
downscope: tuple[ScopeRecord, ...] = ()
|
|
41
|
+
space_id: str | None = None
|
|
42
|
+
auth: str | None = None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def validate_genie_source(
|
|
46
|
+
tool_id: str, source: Mapping[str, Any], *, has_downscope: bool = False
|
|
47
|
+
) -> None:
|
|
48
|
+
"""Validate Genie bindings consistently for CLI models and direct manifest reads."""
|
|
49
|
+
kind = source.get("kind")
|
|
50
|
+
if kind not in {"genie_one", "genie_agent"}:
|
|
51
|
+
if "space_id" in source:
|
|
52
|
+
raise ToolManifestError("Only genie_agent bindings accept source.space_id.")
|
|
53
|
+
return
|
|
54
|
+
if not isinstance(tool_id, str) or not re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]{0,47}", tool_id):
|
|
55
|
+
raise ToolManifestError(
|
|
56
|
+
f"Invalid Genie tool id {tool_id!r}: use a function-name prefix of at most 48 "
|
|
57
|
+
"ASCII letters, digits, or underscores, starting with a letter or underscore."
|
|
58
|
+
)
|
|
59
|
+
allowed_fields = {"kind", "space_id"} if kind == "genie_agent" else {"kind"}
|
|
60
|
+
unexpected = source.keys() - allowed_fields
|
|
61
|
+
if unexpected:
|
|
62
|
+
raise ToolManifestError(
|
|
63
|
+
f"{kind} bindings do not accept source fields: {', '.join(sorted(unexpected))}."
|
|
64
|
+
)
|
|
65
|
+
if has_downscope:
|
|
66
|
+
raise ToolManifestError("Genie bindings do not accept policy.downscope.")
|
|
67
|
+
if kind == "genie_agent":
|
|
68
|
+
space_id = source.get("space_id")
|
|
69
|
+
if not isinstance(space_id, str) or not re.fullmatch(r"[0-9a-f]{32}", space_id):
|
|
70
|
+
raise ToolManifestError(
|
|
71
|
+
"Genie Agent space_id must be 32 lowercase hexadecimal characters."
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def project_root() -> pathlib.Path:
|
|
76
|
+
"""Resolve the agent project containing ``agent.toml``.
|
|
77
|
+
|
|
78
|
+
This module ships in the databricks-agentbricks package, not inside the agent project, so locate the
|
|
79
|
+
project relative to where the agent runs (the current working directory) — not this file's
|
|
80
|
+
location. ``AGENTBRICKS_PROJECT_ROOT`` overrides for cases where the process starts elsewhere.
|
|
81
|
+
"""
|
|
82
|
+
configured = os.getenv("AGENTBRICKS_PROJECT_ROOT")
|
|
83
|
+
if configured:
|
|
84
|
+
root = pathlib.Path(configured).expanduser().resolve()
|
|
85
|
+
if (root / "agent.toml").is_file():
|
|
86
|
+
return root
|
|
87
|
+
raise RuntimeError(f"AGENTBRICKS_PROJECT_ROOT has no agent.toml: {root}")
|
|
88
|
+
|
|
89
|
+
cwd = pathlib.Path.cwd().resolve()
|
|
90
|
+
for candidate in (cwd, *cwd.parents):
|
|
91
|
+
if (candidate / "agent.toml").is_file():
|
|
92
|
+
return candidate
|
|
93
|
+
raise RuntimeError(
|
|
94
|
+
"Could not locate agent.toml; set AGENTBRICKS_PROJECT_ROOT to the project root."
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _required_string(value: object, description: str) -> str:
|
|
99
|
+
if not isinstance(value, str) or not value:
|
|
100
|
+
raise RuntimeError(f"agent.toml must declare {description}.")
|
|
101
|
+
return value
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _scope(value: object) -> ScopeRecord:
|
|
105
|
+
if not isinstance(value, dict):
|
|
106
|
+
raise RuntimeError("agent.toml downscope entries must be tables.")
|
|
107
|
+
value = cast(dict[str, Any], value)
|
|
108
|
+
resource = _required_string(value.get("resource"), "a downscope resource")
|
|
109
|
+
kind, separator, resource_value = resource.partition(":")
|
|
110
|
+
if not separator or kind not in {"table", "volume", "workspace"} or not resource_value:
|
|
111
|
+
raise RuntimeError(f"Invalid agent.toml downscope resource: {resource!r}.")
|
|
112
|
+
permission = _required_string(value.get("permission", "read_only"), "a permission")
|
|
113
|
+
if permission not in {"read_only", "read_write"}:
|
|
114
|
+
raise RuntimeError(f"Invalid agent.toml downscope permission: {permission!r}.")
|
|
115
|
+
return ScopeRecord(kind=kind, value=resource_value, permission=permission)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _tool(value: object) -> ToolRecord:
|
|
119
|
+
if not isinstance(value, dict):
|
|
120
|
+
raise RuntimeError("agent.toml tools must be tables.")
|
|
121
|
+
value = cast(dict[str, Any], value)
|
|
122
|
+
source = value.get("source")
|
|
123
|
+
if not isinstance(source, dict):
|
|
124
|
+
raise RuntimeError("Each agent.toml tool must declare a source table.")
|
|
125
|
+
source = cast(dict[str, Any], source)
|
|
126
|
+
policy = value.get("policy", {})
|
|
127
|
+
if not isinstance(policy, dict):
|
|
128
|
+
raise RuntimeError("agent.toml tool policy must be a table.")
|
|
129
|
+
policy = cast(dict[str, Any], policy)
|
|
130
|
+
raw_downscope = policy.get("downscope", [])
|
|
131
|
+
if not isinstance(raw_downscope, list):
|
|
132
|
+
raise RuntimeError("agent.toml policy.downscope must be an array.")
|
|
133
|
+
kind = _required_string(source.get("kind"), "a tool source kind")
|
|
134
|
+
tool_id = _required_string(value.get("id"), "a tool id")
|
|
135
|
+
validate_genie_source(tool_id, source, has_downscope="downscope" in policy)
|
|
136
|
+
auth = value.get("auth")
|
|
137
|
+
if auth is not None and auth not in ("user", "app"):
|
|
138
|
+
raise ToolManifestError("Tool auth must be 'user' or 'app'.")
|
|
139
|
+
if kind == "uc_function" and auth == "user":
|
|
140
|
+
raise ToolManifestError("UC function auth supports only app/default identity.")
|
|
141
|
+
if kind == "python":
|
|
142
|
+
raise ToolManifestError(
|
|
143
|
+
"Python tools are code-first and cannot be declared in agent.toml. "
|
|
144
|
+
"Remove this entry; decorated tools in agent/tools remain active."
|
|
145
|
+
)
|
|
146
|
+
record = ToolRecord(
|
|
147
|
+
id=tool_id,
|
|
148
|
+
kind=kind,
|
|
149
|
+
service=source.get("service") if isinstance(source.get("service"), str) else None,
|
|
150
|
+
function=source.get("function") if isinstance(source.get("function"), str) else None,
|
|
151
|
+
space_id=source.get("space_id") if isinstance(source.get("space_id"), str) else None,
|
|
152
|
+
downscope=tuple(_scope(item) for item in raw_downscope),
|
|
153
|
+
auth=auth,
|
|
154
|
+
)
|
|
155
|
+
if record.kind == "sandbox" and (record.service != "system.ai.sandbox" or not record.downscope):
|
|
156
|
+
raise RuntimeError("Sandbox bindings require system.ai.sandbox and a downscope.")
|
|
157
|
+
if record.kind == "mcp" and not record.service:
|
|
158
|
+
raise RuntimeError("MCP bindings require source.service.")
|
|
159
|
+
if record.kind == "uc_function" and not record.function:
|
|
160
|
+
raise RuntimeError("UC function bindings require source.function.")
|
|
161
|
+
if record.kind not in {"sandbox", "mcp", "uc_function", "genie_one", "genie_agent"}:
|
|
162
|
+
raise RuntimeError(f"Unsupported agent.toml tool kind: {record.kind!r}.")
|
|
163
|
+
if record.kind != "sandbox" and record.downscope:
|
|
164
|
+
raise RuntimeError("Only sandbox bindings accept policy.downscope.")
|
|
165
|
+
return record
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def load_tools(*, expected_framework: str) -> tuple[ToolRecord, ...]:
|
|
169
|
+
"""Load a fresh immutable view so direct manifest edits apply on the next request."""
|
|
170
|
+
path = project_root() / "agent.toml"
|
|
171
|
+
try:
|
|
172
|
+
with path.open("rb") as input_file:
|
|
173
|
+
document: dict[str, Any] = tomllib.load(input_file)
|
|
174
|
+
except (OSError, tomllib.TOMLDecodeError) as exc:
|
|
175
|
+
raise RuntimeError(f"Could not read {path}: {exc}") from exc
|
|
176
|
+
if document.get("schema_version") != 1:
|
|
177
|
+
raise RuntimeError(f"Unsupported agent.toml schema in {path}; expected schema_version = 1.")
|
|
178
|
+
agent = document.get("agent")
|
|
179
|
+
if not isinstance(agent, dict) or agent.get("framework") != expected_framework:
|
|
180
|
+
actual = agent.get("framework") if isinstance(agent, dict) else None
|
|
181
|
+
raise RuntimeError(
|
|
182
|
+
f"agent.toml framework {actual!r} does not match runtime {expected_framework!r}."
|
|
183
|
+
)
|
|
184
|
+
raw_tools = document.get("tools", [])
|
|
185
|
+
if not isinstance(raw_tools, list):
|
|
186
|
+
raise RuntimeError("agent.toml tools must be an array of tables.")
|
|
187
|
+
tools = tuple(_tool(item) for item in raw_tools)
|
|
188
|
+
ids = [tool.id for tool in tools]
|
|
189
|
+
if len(ids) != len(set(ids)):
|
|
190
|
+
raise RuntimeError("agent.toml tool ids must be unique.")
|
|
191
|
+
return tools
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def resolve_memory_store(explicit: str | None = None) -> str | None:
|
|
195
|
+
"""The memory store id: ``explicit`` arg → ``AGENT_MEMORY_STORE`` env → None.
|
|
196
|
+
|
|
197
|
+
The one place the store-resolution precedence lives, shared by the framework adapters and the
|
|
198
|
+
chat-app UI so they always agree on which store is in effect. None means "no memory store".
|
|
199
|
+
"""
|
|
200
|
+
return explicit or os.getenv(MEMORY_STORE_ENV) or None
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def resolve_session_store(explicit: str | None = None) -> str | None:
|
|
204
|
+
"""The session store name: ``explicit`` arg → ``AGENT_SESSION_STORE`` env → None.
|
|
205
|
+
|
|
206
|
+
Shared precedence for the framework adapters and the chat-app UI (see ``resolve_memory_store``).
|
|
207
|
+
None means "no session store" (in-memory).
|
|
208
|
+
"""
|
|
209
|
+
return explicit or os.getenv(SESSION_STORE_ENV) or None
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def downscope_wire(tool: ToolRecord) -> dict[str, list[dict[str, str]]]:
|
|
213
|
+
"""Convert protected policy into the system.ai.sandbox MCP ``_meta`` shape."""
|
|
214
|
+
fields = {
|
|
215
|
+
"table": ("tables", "name"),
|
|
216
|
+
"volume": ("volumes", "name"),
|
|
217
|
+
"workspace": ("workspace_paths", "path"),
|
|
218
|
+
}
|
|
219
|
+
result: dict[str, list[dict[str, str]]] = {}
|
|
220
|
+
for scope in tool.downscope:
|
|
221
|
+
group, field = fields[scope.kind]
|
|
222
|
+
result.setdefault(group, []).append({field: scope.value, "permission": scope.permission})
|
|
223
|
+
return result
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""MLflow tracing setup — opt-in, enabled when MLflow has both a destination and an experiment.
|
|
2
|
+
|
|
3
|
+
Tracing turns on only when a full config is present: a destination (``MLFLOW_TRACKING_URI`` or
|
|
4
|
+
``MLFLOW_TRACING_DESTINATION``) AND an experiment (``MLFLOW_EXPERIMENT_ID`` or
|
|
5
|
+
``MLFLOW_EXPERIMENT_NAME``) — whichever pair the user or the Apps resource binding provides. MLflow
|
|
6
|
+
resolves the specific value itself; this only decides on/off. Requiring both halves avoids the
|
|
7
|
+
half-configured case where traces silently export to a local file store instead of the workspace.
|
|
8
|
+
When unconfigured, tracing is disabled outright so ``start_trace`` (which agents open around each
|
|
9
|
+
invocation) has nothing to export to.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import contextlib
|
|
13
|
+
import os
|
|
14
|
+
from collections.abc import Callable, Iterator
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
import mlflow
|
|
18
|
+
|
|
19
|
+
# Destination and experiment can each be named more than one way; accept any combination MLflow
|
|
20
|
+
# understands (see mlflow.tracking.fluent._get_experiment_id_from_env for the experiment resolution).
|
|
21
|
+
_DESTINATION_VARS = ("MLFLOW_TRACKING_URI", "MLFLOW_TRACING_DESTINATION")
|
|
22
|
+
_EXPERIMENT_VARS = ("MLFLOW_EXPERIMENT_ID", "MLFLOW_EXPERIMENT_NAME")
|
|
23
|
+
|
|
24
|
+
# Snapshotted once by configure_tracing() at startup (after .env is loaded) rather than at import, so
|
|
25
|
+
# this module has no import-time side effects and load order does not matter.
|
|
26
|
+
_enabled = False
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def configure_tracing(autolog: Callable[[], None] | None = None) -> None:
|
|
30
|
+
"""Enable MLflow tracing for the agent. Call once at startup.
|
|
31
|
+
|
|
32
|
+
Reads the MLflow destination/experiment from the environment and no-ops (disables tracing) when
|
|
33
|
+
they are absent, so it is safe to call unconditionally. ``autolog`` is the framework's MLflow
|
|
34
|
+
autolog entry point (e.g. ``mlflow.langchain.autolog``), called only when tracing is enabled;
|
|
35
|
+
framework adapters bind it so callers get a zero-arg ``configure_tracing()``.
|
|
36
|
+
"""
|
|
37
|
+
global _enabled
|
|
38
|
+
has_destination = any(os.getenv(v) for v in _DESTINATION_VARS)
|
|
39
|
+
has_experiment = any(os.getenv(v) for v in _EXPERIMENT_VARS)
|
|
40
|
+
_enabled = has_destination and has_experiment
|
|
41
|
+
if _enabled:
|
|
42
|
+
if autolog is not None:
|
|
43
|
+
autolog()
|
|
44
|
+
else:
|
|
45
|
+
# Agents wrap each invocation in start_trace; without an experiment it would try to export to a
|
|
46
|
+
# missing one (INVALID_PARAMETER_VALUE), so disable tracing outright.
|
|
47
|
+
mlflow.tracing.disable()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@contextlib.contextmanager
|
|
51
|
+
def start_trace(name: str, inputs: Any = None, session_id: str | None = None) -> Iterator[Any]:
|
|
52
|
+
"""Start a trace by opening a root MLflow span around an operation (no-op when tracing is disabled).
|
|
53
|
+
|
|
54
|
+
Use this wherever tracing needs a root span to already exist. Framework autolog records spans
|
|
55
|
+
only when a trace is active and does not always start one itself (notably for streaming
|
|
56
|
+
invocations), so without a root span the operation can produce zero traces. Wrapping it in this
|
|
57
|
+
span gives autolog a parent to nest its spans under.
|
|
58
|
+
|
|
59
|
+
When ``session_id`` is given, the span's trace is tagged with it so traces group by session.
|
|
60
|
+
|
|
61
|
+
Yields the live span (so the caller can set outputs) when enabled, else ``None``.
|
|
62
|
+
"""
|
|
63
|
+
if not _enabled:
|
|
64
|
+
yield None
|
|
65
|
+
return
|
|
66
|
+
with mlflow.start_span(name=name) as span:
|
|
67
|
+
if session_id:
|
|
68
|
+
mlflow.update_current_trace(metadata={"mlflow.trace.session": session_id})
|
|
69
|
+
if inputs is not None:
|
|
70
|
+
span.set_inputs(inputs)
|
|
71
|
+
yield span
|