databricks-agentbricks 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (159) hide show
  1. databricks_agentbricks/__init__.py +1 -0
  2. databricks_agentbricks/_group.py +217 -0
  3. databricks_agentbricks/agent_project.py +657 -0
  4. databricks_agentbricks/app_resources.py +139 -0
  5. databricks_agentbricks/cli/__init__.py +12 -0
  6. databricks_agentbricks/cli/app.py +108 -0
  7. databricks_agentbricks/cli/app_auth.py +198 -0
  8. databricks_agentbricks/cli/auth.py +142 -0
  9. databricks_agentbricks/cli/deploy.py +954 -0
  10. databricks_agentbricks/cli/dev.py +310 -0
  11. databricks_agentbricks/cli/endpoint.py +145 -0
  12. databricks_agentbricks/cli/endpoint_examples.py +33 -0
  13. databricks_agentbricks/cli/endpoint_output.py +53 -0
  14. databricks_agentbricks/cli/endpoint_request.py +71 -0
  15. databricks_agentbricks/cli/endpoint_transport.py +148 -0
  16. databricks_agentbricks/cli/help.py +467 -0
  17. databricks_agentbricks/cli/init.py +496 -0
  18. databricks_agentbricks/cli/mcp.py +75 -0
  19. databricks_agentbricks/cli/memory.py +520 -0
  20. databricks_agentbricks/cli/sandbox.py +823 -0
  21. databricks_agentbricks/cli/sessions.py +588 -0
  22. databricks_agentbricks/cli/tools.py +456 -0
  23. databricks_agentbricks/cli/tracing.py +724 -0
  24. databricks_agentbricks/databricks_cli.py +38 -0
  25. databricks_agentbricks/errors.py +114 -0
  26. databricks_agentbricks/lakebase_runtime_store.py +107 -0
  27. databricks_agentbricks/legacy_lakebase_runtime_store.py +74 -0
  28. databricks_agentbricks/project_config.py +157 -0
  29. databricks_agentbricks/project_types.py +55 -0
  30. databricks_agentbricks/py.typed +0 -0
  31. databricks_agentbricks/render.py +349 -0
  32. databricks_agentbricks/templates/agent-bricks-migrate/SKILL.md +101 -0
  33. databricks_agentbricks/templates/agent-langgraph/.env.example +36 -0
  34. databricks_agentbricks/templates/agent-langgraph/.gitignore +199 -0
  35. databricks_agentbricks/templates/agent-langgraph/AGENTKIT_CONTRACT.md +95 -0
  36. databricks_agentbricks/templates/agent-langgraph/AGENTS.md +31 -0
  37. databricks_agentbricks/templates/agent-langgraph/CLAUDE.md +1 -0
  38. databricks_agentbricks/templates/agent-langgraph/README.md +141 -0
  39. databricks_agentbricks/templates/agent-langgraph/agent/__init__.py +0 -0
  40. databricks_agentbricks/templates/agent-langgraph/agent/agent.py +177 -0
  41. databricks_agentbricks/templates/agent-langgraph/agent/mcps.py +27 -0
  42. databricks_agentbricks/templates/agent-langgraph/agent/tools/__init__.py +24 -0
  43. databricks_agentbricks/templates/agent-langgraph/agent/tools/sample_tool.py +15 -0
  44. databricks_agentbricks/templates/agent-langgraph/agent/tools/send_message.py +17 -0
  45. databricks_agentbricks/templates/agent-langgraph/app.yaml +15 -0
  46. databricks_agentbricks/templates/agent-langgraph/pyproject.toml +42 -0
  47. databricks_agentbricks/templates/agent-langgraph/runtime/__init__.py +0 -0
  48. databricks_agentbricks/templates/agent-langgraph/runtime/adapter.py +144 -0
  49. databricks_agentbricks/templates/agent-langgraph/runtime/main.py +25 -0
  50. databricks_agentbricks/templates/agent-langgraph/tests/__init__.py +0 -0
  51. databricks_agentbricks/templates/agent-langgraph/tests/test_agent.py +211 -0
  52. databricks_agentbricks/templates/agent-langgraph/tests/test_runtime.py +19 -0
  53. databricks_agentbricks/templates/agent-langgraph/tests/test_workspace.py +28 -0
  54. databricks_agentbricks/templates/agent-openai/.env.example +37 -0
  55. databricks_agentbricks/templates/agent-openai/.gitignore +199 -0
  56. databricks_agentbricks/templates/agent-openai/AGENTKIT_CONTRACT.md +107 -0
  57. databricks_agentbricks/templates/agent-openai/AGENTS.md +76 -0
  58. databricks_agentbricks/templates/agent-openai/CLAUDE.md +1 -0
  59. databricks_agentbricks/templates/agent-openai/README.md +143 -0
  60. databricks_agentbricks/templates/agent-openai/agent/__init__.py +0 -0
  61. databricks_agentbricks/templates/agent-openai/agent/agent.py +205 -0
  62. databricks_agentbricks/templates/agent-openai/agent/mcps.py +28 -0
  63. databricks_agentbricks/templates/agent-openai/agent/tools/__init__.py +24 -0
  64. databricks_agentbricks/templates/agent-openai/agent/tools/sample_tool.py +15 -0
  65. databricks_agentbricks/templates/agent-openai/agent/tools/send_message.py +18 -0
  66. databricks_agentbricks/templates/agent-openai/app.yaml +15 -0
  67. databricks_agentbricks/templates/agent-openai/pyproject.toml +42 -0
  68. databricks_agentbricks/templates/agent-openai/runtime/__init__.py +0 -0
  69. databricks_agentbricks/templates/agent-openai/runtime/adapter.py +188 -0
  70. databricks_agentbricks/templates/agent-openai/runtime/main.py +25 -0
  71. databricks_agentbricks/templates/agent-openai/tests/__init__.py +0 -0
  72. databricks_agentbricks/templates/agent-openai/tests/test_agent.py +326 -0
  73. databricks_agentbricks/templates/agent-openai/tests/test_runtime.py +19 -0
  74. databricks_agentbricks/templates/agent-openai/tests/test_workspace.py +28 -0
  75. databricks_agentbricks/templates/custom-agent-langgraph/.env.example +1 -0
  76. databricks_agentbricks/templates/custom-agent-langgraph/.gitignore +11 -0
  77. databricks_agentbricks/templates/custom-agent-langgraph/README.md +22 -0
  78. databricks_agentbricks/templates/custom-agent-langgraph/agent/__init__.py +0 -0
  79. databricks_agentbricks/templates/custom-agent-langgraph/agent/agent.py +27 -0
  80. databricks_agentbricks/templates/custom-agent-langgraph/app.yaml +1 -0
  81. databricks_agentbricks/templates/custom-agent-langgraph/pyproject.toml +38 -0
  82. databricks_agentbricks/templates/custom-agent-langgraph/runtime/__init__.py +0 -0
  83. databricks_agentbricks/templates/custom-agent-langgraph/runtime/main.py +31 -0
  84. databricks_agentbricks/templates/custom-agent-langgraph/tests/test_runtime.py +29 -0
  85. databricks_agentbricks/templates/custom-agent-openai/.env.example +1 -0
  86. databricks_agentbricks/templates/custom-agent-openai/.gitignore +11 -0
  87. databricks_agentbricks/templates/custom-agent-openai/README.md +22 -0
  88. databricks_agentbricks/templates/custom-agent-openai/agent/__init__.py +0 -0
  89. databricks_agentbricks/templates/custom-agent-openai/agent/agent.py +29 -0
  90. databricks_agentbricks/templates/custom-agent-openai/app.yaml +1 -0
  91. databricks_agentbricks/templates/custom-agent-openai/pyproject.toml +38 -0
  92. databricks_agentbricks/templates/custom-agent-openai/runtime/__init__.py +0 -0
  93. databricks_agentbricks/templates/custom-agent-openai/runtime/main.py +31 -0
  94. databricks_agentbricks/templates/custom-agent-openai/tests/test_runtime.py +29 -0
  95. databricks_agentbricks/templates/sandbox_mcp.py +39 -0
  96. databricks_agentbricks/templates/sandbox_mcp_langgraph.py +39 -0
  97. databricks_agentbricks/templates/ui/agent-langgraph/CHAT_APP.md +49 -0
  98. databricks_agentbricks/templates/ui/agent-langgraph/runtime/main.py +26 -0
  99. databricks_agentbricks/templates/ui/agent-langgraph/runtime/ui.py +528 -0
  100. databricks_agentbricks/templates/ui/agent-langgraph/tests/test_demo_ui.py +542 -0
  101. databricks_agentbricks/templates/ui/agent-langgraph/ui/agentbricks-mark.png +0 -0
  102. databricks_agentbricks/templates/ui/agent-langgraph/ui/app.js +1464 -0
  103. databricks_agentbricks/templates/ui/agent-langgraph/ui/index.html +281 -0
  104. databricks_agentbricks/templates/ui/agent-langgraph/ui/styles.css +1913 -0
  105. databricks_agentbricks/templates/ui/agent-openai/CHAT_APP.md +54 -0
  106. databricks_agentbricks/templates/ui/agent-openai/runtime/main.py +26 -0
  107. databricks_agentbricks/templates/ui/agent-openai/runtime/ui.py +521 -0
  108. databricks_agentbricks/templates/ui/agent-openai/tests/test_demo_ui.py +512 -0
  109. databricks_agentbricks/templates/ui/agent-openai/ui/agentbricks-mark.png +0 -0
  110. databricks_agentbricks/templates/ui/agent-openai/ui/app.js +1428 -0
  111. databricks_agentbricks/templates/ui/agent-openai/ui/index.html +281 -0
  112. databricks_agentbricks/templates/ui/agent-openai/ui/styles.css +1913 -0
  113. databricks_agentbricks/theme.py +72 -0
  114. databricks_agentbricks-0.2.0.dist-info/METADATA +897 -0
  115. databricks_agentbricks-0.2.0.dist-info/RECORD +159 -0
  116. databricks_agentbricks-0.2.0.dist-info/WHEEL +4 -0
  117. databricks_agentbricks-0.2.0.dist-info/entry_points.txt +2 -0
  118. databricks_agentbricks-0.2.0.dist-info/licenses/NOTICE +39 -0
  119. databricks_agentkit/__init__.py +67 -0
  120. databricks_agentkit/_api_client.py +673 -0
  121. databricks_agentkit/_pagination.py +25 -0
  122. databricks_agentkit/client.py +31 -0
  123. databricks_agentkit/langgraph/__init__.py +98 -0
  124. databricks_agentkit/langgraph/genie.py +50 -0
  125. databricks_agentkit/langgraph/mcp.py +216 -0
  126. databricks_agentkit/langgraph/memory.py +67 -0
  127. databricks_agentkit/langgraph/session_store.py +485 -0
  128. databricks_agentkit/memory_store.py +371 -0
  129. databricks_agentkit/models.py +330 -0
  130. databricks_agentkit/openai/__init__.py +93 -0
  131. databricks_agentkit/openai/genie.py +50 -0
  132. databricks_agentkit/openai/mcp.py +189 -0
  133. databricks_agentkit/openai/memory.py +67 -0
  134. databricks_agentkit/openai/sessions.py +157 -0
  135. databricks_agentkit/py.typed +0 -0
  136. databricks_agentkit/runtime/ARCHITECTURE.md +64 -0
  137. databricks_agentkit/runtime/README.md +184 -0
  138. databricks_agentkit/runtime/__init__.py +69 -0
  139. databricks_agentkit/runtime/app.py +316 -0
  140. databricks_agentkit/runtime/auth.py +149 -0
  141. databricks_agentkit/runtime/durability/__init__.py +1 -0
  142. databricks_agentkit/runtime/durability/execution.py +133 -0
  143. databricks_agentkit/runtime/durability/heartbeat.py +64 -0
  144. databricks_agentkit/runtime/durability/lakebase_runtime_store.py +648 -0
  145. databricks_agentkit/runtime/durability/recovery.py +70 -0
  146. databricks_agentkit/runtime/durability/store.py +33 -0
  147. databricks_agentkit/runtime/execution.py +160 -0
  148. databricks_agentkit/runtime/genie.py +224 -0
  149. databricks_agentkit/runtime/mcp_auth.py +114 -0
  150. databricks_agentkit/runtime/model_services.py +90 -0
  151. databricks_agentkit/runtime/runtime.py +273 -0
  152. databricks_agentkit/runtime/session_store_client.py +120 -0
  153. databricks_agentkit/runtime/store.py +287 -0
  154. databricks_agentkit/runtime/tool_manifest.py +223 -0
  155. databricks_agentkit/runtime/tracing.py +71 -0
  156. databricks_agentkit/runtime/types.py +118 -0
  157. databricks_agentkit/runtime/workspace.py +27 -0
  158. databricks_agentkit/session_store.py +434 -0
  159. databricks_agentkit/timefmt.py +85 -0
@@ -0,0 +1,528 @@
1
+ """Browser UI and managed-state demo controls for an Agent Bricks agent project."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import json
7
+ import os
8
+ import uuid
9
+ from functools import lru_cache
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+ from fastapi import FastAPI, HTTPException, Query, Request
14
+ from fastapi.responses import FileResponse, JSONResponse
15
+ from fastapi.staticfiles import StaticFiles
16
+ from pydantic import BaseModel, Field
17
+
18
+ from databricks_agentkit import workspace_client
19
+ from databricks_agentkit.runtime.model_services import list_ai_gateway_model_services
20
+ from databricks_agentkit.runtime.store import runtime_store_is_persistent_environment
21
+
22
+ _UI_ROOT = Path(__file__).resolve().parent.parent / "ui"
23
+ _INSTANCE_ID = uuid.uuid4().hex[:12] # identifies this process in the UI
24
+ _AGENTS_API = "/api/2.0/agents"
25
+ _MESSAGE_ROLES = {
26
+ "ai",
27
+ "assistant",
28
+ "developer",
29
+ "function",
30
+ "human",
31
+ "human_decision",
32
+ "system",
33
+ "tool",
34
+ "user",
35
+ }
36
+
37
+
38
+ class MemoryEntryRequest(BaseModel):
39
+ path: str = Field(min_length=1, pattern=r"^/")
40
+ content: str = Field(min_length=1)
41
+ description: str | None = None
42
+
43
+
44
+ class MemorySearchRequest(BaseModel):
45
+ query: str = Field(min_length=1)
46
+ limit: int = Field(default=10, ge=1, le=100)
47
+ actor: str | None = None
48
+
49
+
50
+ class SessionItemsRequest(BaseModel):
51
+ items: list[dict[str, Any]] = Field(min_length=1)
52
+
53
+
54
+ _USER_HEADERS = ("x-forwarded-email", "x-forwarded-user")
55
+
56
+
57
+ def _memory_store() -> str:
58
+ # Same resolution the agent uses (AGENT_MEMORY_STORE env → agent.toml binding), so the demo
59
+ # panels reflect exactly the store the agent reads/writes.
60
+ from databricks_agentkit.runtime.tool_manifest import resolve_memory_store
61
+
62
+ return (resolve_memory_store() or "").strip().strip("/")
63
+
64
+
65
+ def _session_store() -> str:
66
+ from databricks_agentkit.runtime.tool_manifest import resolve_session_store
67
+
68
+ return (resolve_session_store() or "").strip()
69
+
70
+
71
+ def _request_actor(request: Request) -> str:
72
+ """The actor for a demo request — the signed-in user, so the panels show that user's own data.
73
+
74
+ Mirrors how the agent resolves its actor (same forwarded-identity headers), so the memory and
75
+ session views here list exactly what the agent reads/writes for the current user. Falls back to
76
+ ``"agent"`` locally / when unauthenticated.
77
+ """
78
+ for header in _USER_HEADERS:
79
+ if value := request.headers.get(header):
80
+ return value
81
+ return "agent"
82
+
83
+
84
+ def _request_session_id(request: Request) -> str:
85
+ """Read the chat session selected by the browser, falling back to the router cookie locally."""
86
+ session_id = request.query_params.get("session_id") or getattr(
87
+ request.state, "session_id", None
88
+ )
89
+ if not session_id:
90
+ raise HTTPException(status_code=400, detail="session_id is required")
91
+ return str(session_id)
92
+
93
+
94
+ def _is_deployed() -> bool:
95
+ app_url = os.getenv("DATABRICKS_APP_URL", "")
96
+ is_local = app_url.startswith(("http://localhost", "http://127.0.0.1"))
97
+ return bool(os.getenv("DATABRICKS_APP_NAME")) and not is_local
98
+
99
+
100
+ # Tracing turns on only with both a destination and an experiment, mirroring
101
+ # the runtime tracing setup so the UI card matches what the agent actually does.
102
+ _TRACING_DESTINATION_VARS = ("MLFLOW_TRACKING_URI", "MLFLOW_TRACING_DESTINATION")
103
+
104
+
105
+ def _workspace_host() -> str:
106
+ """Best-effort workspace host for building MLflow links; never raises."""
107
+ try:
108
+ host = workspace_client().config.host or ""
109
+ except Exception: # noqa: BLE001 - host is best-effort; a failure must not break the config route
110
+ host = os.getenv("DATABRICKS_HOST", "")
111
+ return host.rstrip("/")
112
+
113
+
114
+ def _tracing() -> dict:
115
+ """Whether MLflow tracing is configured, plus a best-effort link to its experiment."""
116
+ destination = ""
117
+ for var in _TRACING_DESTINATION_VARS:
118
+ if value := os.getenv(var):
119
+ destination = value
120
+ break
121
+ experiment_id = os.getenv("MLFLOW_EXPERIMENT_ID", "").strip()
122
+ experiment_name = os.getenv("MLFLOW_EXPERIMENT_NAME", "").strip()
123
+ enabled = bool(destination) and bool(experiment_id or experiment_name)
124
+ url: str | None = None
125
+ if enabled:
126
+ if destination.startswith(("http://", "https://")) and "databricks" not in destination:
127
+ # A local OSS MLflow server: link into its own UI.
128
+ base = destination.rstrip("/")
129
+ url = f"{base}/#/experiments/{experiment_id}" if experiment_id else base
130
+ else:
131
+ host = _workspace_host()
132
+ if host:
133
+ url = (
134
+ f"{host}/ml/experiments/{experiment_id}"
135
+ if experiment_id
136
+ else f"{host}/ml/experiments"
137
+ )
138
+ return {"enabled": enabled, "experiment_id": experiment_id or None, "url": url}
139
+
140
+
141
+ # Cap the picker: the gateway's schema can list more models than belong in a dropdown.
142
+ _MODEL_LIMIT = 20
143
+
144
+
145
+ def _default_model() -> str:
146
+ """The agent's configured default endpoint (``agent.agent.MODEL``), imported lazily.
147
+
148
+ Imported inside the function, not at module load, so the light UI import path doesn't pull in the
149
+ agent stack (matching ``_checkpoint_history`` below).
150
+ """
151
+ from agent.agent import MODEL
152
+
153
+ return MODEL
154
+
155
+
156
+ def _rank_models(default: str, names: list[str]) -> list[str]:
157
+ """Pin the default first, then the remaining gateway models alphabetically, without duplicates.
158
+
159
+ Truncation therefore only ever sheds models beyond the cap, never the configured default.
160
+ """
161
+ ordered = [default, *sorted(n for n in names if n != default)]
162
+ seen: set[str] = set()
163
+ return [n for n in ordered if not (n in seen or seen.add(n))]
164
+
165
+
166
+ def _discover_chat_models() -> list[str]:
167
+ """The AI Gateway chat models for the picker: default first, then the rest, capped.
168
+
169
+ Best-effort: if listing fails (missing permission, repeated transient errors), fall back to just
170
+ the default so the picker still works. The default is always present and first.
171
+ """
172
+ default = _default_model()
173
+ try:
174
+ names = list_ai_gateway_model_services(workspace_client())
175
+ except Exception: # noqa: BLE001 - a broken listing must not break the whole config endpoint
176
+ names = []
177
+ return _rank_models(default, names)[:_MODEL_LIMIT]
178
+
179
+
180
+ class _ManagedStateClient:
181
+ def __init__(self) -> None:
182
+ self._workspace = workspace_client()
183
+
184
+ def _do(
185
+ self,
186
+ method: str,
187
+ path: str,
188
+ *,
189
+ query: dict[str, Any] | None = None,
190
+ body: dict[str, Any] | None = None,
191
+ ) -> dict:
192
+ result = self._workspace.api_client.do(method, path, query=query, body=body)
193
+ if not isinstance(result, dict):
194
+ raise RuntimeError(
195
+ f"Expected an object response from {path}, got {type(result).__name__}"
196
+ )
197
+ return result
198
+
199
+ def create_memory_entry(self, actor: str, request: MemoryEntryRequest, session_id: str) -> dict:
200
+ body = {
201
+ "actor_id": actor,
202
+ "path": request.path,
203
+ "content": request.content,
204
+ "session_id": session_id,
205
+ }
206
+ if request.description:
207
+ body["description"] = request.description
208
+ return self._do("POST", f"{_AGENTS_API}/memory-stores/{_memory_store()}/entries", body=body)
209
+
210
+ def list_memory_entries(self, actor: str, path_prefix: str | None = None) -> dict:
211
+ query = {"actor_id": actor, "page_size": 100}
212
+ if path_prefix:
213
+ query["path_prefix"] = path_prefix
214
+ return self._do(
215
+ "GET", f"{_AGENTS_API}/memory-stores/{_memory_store()}/entries", query=query
216
+ )
217
+
218
+ def search_memory_entries(self, actor: str, request: MemorySearchRequest) -> dict:
219
+ return self._do(
220
+ "POST",
221
+ f"{_AGENTS_API}/memory-stores/{_memory_store()}/entries:search",
222
+ body={
223
+ "actor_id": actor,
224
+ "query": request.query,
225
+ "limit": request.limit,
226
+ },
227
+ )
228
+
229
+ def ensure_session(self, actor: str, session_id: str) -> dict:
230
+ try:
231
+ return self._do(
232
+ "POST",
233
+ f"{_AGENTS_API}/session-stores/{_session_store()}/sessions",
234
+ query={"session_id": session_id},
235
+ body={
236
+ "actor_id": actor,
237
+ "metadata": {"client": "agentbricks-demo-ui"},
238
+ },
239
+ )
240
+ except Exception as exc:
241
+ code = str(getattr(exc, "error_code", "")).upper()
242
+ already_exists = code in {"ALREADY_EXISTS", "RESOURCE_ALREADY_EXISTS"}
243
+ if not already_exists and "already exists" not in str(exc).lower():
244
+ raise
245
+ return self._do(
246
+ "GET",
247
+ f"{_AGENTS_API}/session-stores/{_session_store()}/sessions/{session_id}",
248
+ )
249
+
250
+ def get_session(self, session_id: str) -> dict:
251
+ return self._do(
252
+ "GET",
253
+ f"{_AGENTS_API}/session-stores/{_session_store()}/sessions/{session_id}",
254
+ )
255
+
256
+ def list_sessions(self, actor: str) -> dict:
257
+ return self._do(
258
+ "GET",
259
+ f"{_AGENTS_API}/session-stores/{_session_store()}/sessions",
260
+ query={
261
+ "filter": f"actor_id = {json.dumps(actor)}",
262
+ "order_by": "last_activity_time desc",
263
+ "page_size": 50,
264
+ },
265
+ )
266
+
267
+ def append_session_items(self, session_id: str, items: list[dict[str, Any]]) -> dict:
268
+ return self._do(
269
+ "POST",
270
+ f"{_AGENTS_API}/session-stores/{_session_store()}/sessions/{session_id}/items:append",
271
+ body={"items": [{"data": item} for item in items]},
272
+ )
273
+
274
+ def list_session_items(self, session_id: str) -> dict:
275
+ return self._do(
276
+ "GET",
277
+ f"{_AGENTS_API}/session-stores/{_session_store()}/sessions/{session_id}/items",
278
+ query={"order_by": "create_time asc", "page_size": 100},
279
+ )
280
+
281
+
282
+ @lru_cache(maxsize=1)
283
+ def _state_client() -> _ManagedStateClient:
284
+ return _ManagedStateClient()
285
+
286
+
287
+ async def _managed_call(operation, *args):
288
+ try:
289
+ return await asyncio.to_thread(operation, *args)
290
+ except HTTPException:
291
+ raise
292
+ except Exception as exc:
293
+ code = getattr(exc, "error_code", None)
294
+ detail = f"{code}: {exc}" if code else str(exc)
295
+ raise HTTPException(status_code=502, detail=detail) from exc
296
+
297
+
298
+ def _require_memory() -> None:
299
+ if not _memory_store():
300
+ raise HTTPException(
301
+ status_code=503,
302
+ detail="No memory store configured. Run `ab memory bind <store>`.",
303
+ )
304
+
305
+
306
+ def _require_session() -> None:
307
+ if not _session_store():
308
+ raise HTTPException(
309
+ status_code=503,
310
+ detail="No session store configured. Run `ab sessions bind <store>`.",
311
+ )
312
+
313
+
314
+ async def _checkpoint_history(session_id: str, actor: str) -> dict[str, Any]:
315
+ from agent.agent import create_agent_graph
316
+
317
+ from databricks_agentkit.langgraph.session_store import thread_config
318
+
319
+ graph = await create_agent_graph(actor)
320
+ snapshot = await graph.aget_state(thread_config(session_id, actor))
321
+ values = snapshot.values if isinstance(snapshot.values, dict) else {}
322
+ items = []
323
+ for index, message in enumerate(values.get("messages", [])):
324
+ data = message.model_dump() if hasattr(message, "model_dump") else message
325
+ items.append(
326
+ {
327
+ "item_id": str(getattr(message, "id", None) or index),
328
+ "data": data if isinstance(data, dict) else {"content": str(data)},
329
+ }
330
+ )
331
+ interrupts = [
332
+ {"id": interrupt.id, "value": interrupt.value}
333
+ for task in getattr(snapshot, "tasks", ())
334
+ for interrupt in getattr(task, "interrupts", ())
335
+ ]
336
+ return {"session_id": session_id, "session_items": items, "interrupts": interrupts}
337
+
338
+
339
+ def _chat_sessions(result: dict[str, Any]) -> list[dict[str, Any]]:
340
+ sessions = []
341
+ for session in result.get("sessions", []):
342
+ if not isinstance(session, dict):
343
+ continue
344
+ metadata = session.get("metadata")
345
+ metadata = metadata if isinstance(metadata, dict) else {}
346
+ if metadata.get("public_session_id"):
347
+ continue
348
+ sessions.append(session)
349
+ return sessions
350
+
351
+
352
+ def _chat_session_items(result: dict[str, Any]) -> dict[str, Any]:
353
+ items = []
354
+ for item in result.get("session_items", []):
355
+ if not isinstance(item, dict):
356
+ continue
357
+ data = item.get("data")
358
+ if not isinstance(data, dict) or data.get("event_type") or "content" not in data:
359
+ continue
360
+ role = str(data.get("role") or data.get("type") or "").lower()
361
+ if role in _MESSAGE_ROLES:
362
+ items.append(item)
363
+ return {**result, "session_items": items}
364
+
365
+
366
+ def install_ui(app: FastAPI) -> None:
367
+ """Mount the Agent Bricks UI and its runtime control endpoints."""
368
+ app.mount("/ui-assets", StaticFiles(directory=_UI_ROOT), name="agentbricks-demo-ui-assets")
369
+
370
+ @app.middleware("http")
371
+ async def disable_ui_caching(request: Request, call_next):
372
+ response = await call_next(request)
373
+ if request.url.path == "/" or request.url.path.startswith("/ui-assets/"):
374
+ response.headers["Cache-Control"] = "no-store"
375
+ return response
376
+
377
+ @app.get("/", include_in_schema=False)
378
+ async def index() -> FileResponse:
379
+ return FileResponse(_UI_ROOT / "index.html")
380
+
381
+ @app.get("/api/ui/config", include_in_schema=False)
382
+ async def ui_config(request: Request) -> dict:
383
+ actor = _request_actor(request)
384
+ memory_store = _memory_store()
385
+ session_store = _session_store()
386
+ default_model = _default_model()
387
+ runtime_store_persistent = runtime_store_is_persistent_environment()
388
+ runtime_store_mode = (
389
+ "Runtime Store" if runtime_store_persistent else "In-process Runtime Store"
390
+ )
391
+ return {
392
+ "session_id": _request_session_id(request),
393
+ "instance_id": _INSTANCE_ID,
394
+ "viewer": actor if actor != "agent" else "Local developer",
395
+ "deployed": _is_deployed(),
396
+ "models": {"default": default_model, "available": [default_model]},
397
+ "streaming": {
398
+ "enabled": True,
399
+ "transport": "Server-sent events",
400
+ "persistent": runtime_store_persistent,
401
+ "mode": runtime_store_mode,
402
+ },
403
+ "background": {
404
+ "enabled": True,
405
+ "persistent": runtime_store_persistent,
406
+ "mode": runtime_store_mode,
407
+ },
408
+ "session": {
409
+ "durable": bool(session_store),
410
+ "managed": bool(session_store),
411
+ "history": True,
412
+ "mode": "Managed Session Store" if session_store else "In-process checkpointer",
413
+ "store": session_store or None,
414
+ "actor": actor,
415
+ },
416
+ "memory": {
417
+ "enabled": bool(memory_store),
418
+ "store": f"memory-stores/{memory_store}" if memory_store else None,
419
+ "actor": actor,
420
+ },
421
+ "tracing": _tracing(),
422
+ }
423
+
424
+ @app.get("/api/demo/models", include_in_schema=False)
425
+ async def demo_models() -> dict:
426
+ # Model discovery can take several seconds in a large workspace. Keep it separate from the
427
+ # runtime config so the rest of the UI becomes interactive immediately.
428
+ available_models = await asyncio.to_thread(_discover_chat_models)
429
+ return {"default": _default_model(), "available": available_models}
430
+
431
+ @app.post("/api/demo/memory/entries", include_in_schema=False)
432
+ async def create_memory_entry(request: Request, payload: MemoryEntryRequest) -> dict:
433
+ _require_memory()
434
+ return await _managed_call(
435
+ _state_client().create_memory_entry,
436
+ _request_actor(request),
437
+ payload,
438
+ _request_session_id(request),
439
+ )
440
+
441
+ @app.get("/api/demo/memory/entries", include_in_schema=False)
442
+ async def list_memory_entries(
443
+ request: Request,
444
+ path_prefix: str | None = Query(default=None),
445
+ actor: str | None = Query(default=None),
446
+ ) -> dict:
447
+ # The UI can browse another actor's memories by passing ?actor=; default to the viewer.
448
+ _require_memory()
449
+ return await _managed_call(
450
+ _state_client().list_memory_entries, actor or _request_actor(request), path_prefix
451
+ )
452
+
453
+ @app.post("/api/demo/memory/search", include_in_schema=False)
454
+ async def search_memory_entries(request: Request, payload: MemorySearchRequest) -> dict:
455
+ # payload.actor lets the UI search another actor's memories; default to the viewer.
456
+ _require_memory()
457
+ return await _managed_call(
458
+ _state_client().search_memory_entries, payload.actor or _request_actor(request), payload
459
+ )
460
+
461
+ @app.post("/api/demo/sessions", include_in_schema=False)
462
+ async def ensure_session(request: Request) -> dict:
463
+ _require_session()
464
+ return await _managed_call(
465
+ _state_client().ensure_session,
466
+ _request_actor(request),
467
+ _request_session_id(request),
468
+ )
469
+
470
+ @app.get("/api/demo/sessions", include_in_schema=False)
471
+ async def list_sessions(request: Request) -> dict:
472
+ session_id = _request_session_id(request)
473
+ actor = _request_actor(request)
474
+ if not _session_store():
475
+ return {
476
+ "sessions": [
477
+ {
478
+ "session_id": session_id,
479
+ "actor_id": actor,
480
+ "metadata": {"client": "agentbricks-demo-ui-local"},
481
+ }
482
+ ],
483
+ "current_session_id": session_id,
484
+ "managed": False,
485
+ }
486
+ result = await _managed_call(_state_client().list_sessions, actor)
487
+ return {
488
+ **result,
489
+ "sessions": _chat_sessions(result),
490
+ "current_session_id": session_id,
491
+ "managed": True,
492
+ }
493
+
494
+ @app.post("/api/demo/sessions/{session_id}/open", include_in_schema=False)
495
+ async def open_session(request: Request, session_id: str) -> JSONResponse:
496
+ _require_session()
497
+ session = await _managed_call(_state_client().get_session, session_id)
498
+ if session.get("actor_id") != _request_actor(request):
499
+ raise HTTPException(status_code=403, detail="Session belongs to another actor.")
500
+ return JSONResponse(
501
+ {
502
+ "session_id": session_id,
503
+ "previous_session_id": _request_session_id(request),
504
+ "managed": True,
505
+ }
506
+ )
507
+
508
+ @app.get("/api/demo/session", include_in_schema=False)
509
+ async def get_session(request: Request) -> dict:
510
+ _require_session()
511
+ return await _managed_call(_state_client().get_session, _request_session_id(request))
512
+
513
+ @app.post("/api/demo/session/items", include_in_schema=False)
514
+ async def append_session_items(request: Request, payload: SessionItemsRequest) -> dict:
515
+ _require_session()
516
+ return await _managed_call(
517
+ _state_client().append_session_items,
518
+ _request_session_id(request),
519
+ payload.items,
520
+ )
521
+
522
+ @app.get("/api/demo/session/items", include_in_schema=False)
523
+ async def list_session_items(request: Request) -> dict:
524
+ session_id = _request_session_id(request)
525
+ if _session_store():
526
+ result = await _managed_call(_state_client().list_session_items, session_id)
527
+ return _chat_session_items(result)
528
+ return await _checkpoint_history(session_id, _request_actor(request))