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,512 @@
1
+ import pytest
2
+ from fastapi.testclient import TestClient
3
+ from runtime import ui
4
+
5
+ from databricks_agentkit import DurableAgentServer
6
+ from databricks_agentkit.runtime.store import (
7
+ RUNTIME_STORE_DATABASE_ENV,
8
+ RUNTIME_STORE_LAKEBASE_BRANCH_ENV,
9
+ RUNTIME_STORE_LAKEBASE_ENDPOINT_ENV,
10
+ RUNTIME_STORE_LOCAL_ENV,
11
+ RUNTIME_STORE_SCHEMA_ENV,
12
+ RUNTIME_STORE_USERNAME_ENV,
13
+ InMemoryRuntimeStore,
14
+ )
15
+
16
+
17
+ @pytest.fixture(autouse=True)
18
+ def _clear_runtime_store_env(monkeypatch):
19
+ for name in (
20
+ RUNTIME_STORE_LOCAL_ENV,
21
+ RUNTIME_STORE_LAKEBASE_BRANCH_ENV,
22
+ RUNTIME_STORE_DATABASE_ENV,
23
+ RUNTIME_STORE_USERNAME_ENV,
24
+ RUNTIME_STORE_LAKEBASE_ENDPOINT_ENV,
25
+ RUNTIME_STORE_SCHEMA_ENV,
26
+ ):
27
+ monkeypatch.delenv(name, raising=False)
28
+
29
+
30
+ class _FakeStateClient:
31
+ def create_memory_entry(self, actor, request, session_id):
32
+ return {
33
+ "name": "memory-stores/store/entries/entry",
34
+ "session_id": session_id,
35
+ "actor_id": actor,
36
+ **request.model_dump(),
37
+ }
38
+
39
+ def list_memory_entries(self, actor, path_prefix=None):
40
+ return {
41
+ "managed_memory_entries": [
42
+ {"path": f"{path_prefix or ''}/profile.md", "actor_id": actor}
43
+ ]
44
+ }
45
+
46
+ def search_memory_entries(self, actor, request):
47
+ return {
48
+ "managed_memory_entries": [
49
+ {"path": "/profile.md", "content": request.query, "actor_id": actor}
50
+ ]
51
+ }
52
+
53
+ def ensure_session(self, actor, session_id):
54
+ return {"session_id": session_id, "actor_id": actor}
55
+
56
+ def get_session(self, session_id):
57
+ return {"session_id": session_id, "actor_id": "alice"}
58
+
59
+ def list_sessions(self, actor):
60
+ return {
61
+ "sessions": [
62
+ {
63
+ "session_id": "s1",
64
+ "actor_id": "alice",
65
+ "last_activity_time": "2026-08-28T12:00:00Z",
66
+ },
67
+ {
68
+ "session_id": "s2",
69
+ "actor_id": "alice",
70
+ "last_activity_time": "2026-08-27T12:00:00Z",
71
+ },
72
+ {
73
+ "session_id": "public-s1",
74
+ "actor_id": "alice",
75
+ "metadata": {"public_session_id": "s1"},
76
+ "last_activity_time": "2026-08-28T12:01:00Z",
77
+ },
78
+ ]
79
+ }
80
+
81
+ def append_session_items(self, session_id, items):
82
+ return {"session_items": [{"item_id": "1", "data": item} for item in items]}
83
+
84
+ def list_session_items(self, session_id):
85
+ return {
86
+ "session_items": [
87
+ {"item_id": "1", "data": {"role": "user", "content": session_id}},
88
+ {
89
+ "item_id": "2",
90
+ "data": {"type": "assistant", "content": "saved reply"},
91
+ },
92
+ {
93
+ "item_id": "3",
94
+ "data": {"event_type": "checkpoint", "checkpoint_id": "checkpoint-1"},
95
+ },
96
+ ]
97
+ }
98
+
99
+
100
+ async def _session_history(session_id):
101
+ return {
102
+ "session_id": session_id,
103
+ "session_items": [
104
+ {"item_id": "1", "data": {"role": "user", "content": session_id}},
105
+ {"item_id": "2", "data": {"role": "assistant", "content": "in-process reply"}},
106
+ ],
107
+ "interrupts": [],
108
+ }
109
+
110
+
111
+ def _client(monkeypatch, *, configured=False, history=False, session_id="routing-session"):
112
+ if configured:
113
+ monkeypatch.setenv("AGENT_MEMORY_STORE", "store")
114
+ monkeypatch.setenv("AGENT_SESSION_STORE", "sessions")
115
+ monkeypatch.setattr(ui, "_state_client", lambda: _FakeStateClient())
116
+ else:
117
+ monkeypatch.delenv("AGENT_MEMORY_STORE", raising=False)
118
+ monkeypatch.delenv("AGENT_SESSION_STORE", raising=False)
119
+ if history:
120
+ monkeypatch.setattr(ui, "_local_history", _session_history)
121
+ # Keep model discovery deterministic and offline (no AI Gateway listing call).
122
+ monkeypatch.setattr(ui, "_default_model", lambda: "system.ai.claude-sonnet-4-5")
123
+ monkeypatch.setattr(ui, "_discover_chat_models", lambda: ["system.ai.claude-sonnet-4-5"])
124
+
125
+ async def invoke_handler(request, context):
126
+ return {"output": [], "session_id": context.session_id}
127
+
128
+ app = DurableAgentServer(runtime_store=InMemoryRuntimeStore())
129
+ app.invoke(invoke_handler)
130
+ app.recover(invoke_handler)
131
+ ui.install_ui(app)
132
+ client = TestClient(app, base_url="https://testserver")
133
+ client.cookies.set("__Host-databricks-app-router", session_id)
134
+ if configured:
135
+ # The actor is the signed-in user from this forwarded-identity header (ui._request_actor);
136
+ # unconfigured requests have no header and fall back to the "agent" actor.
137
+ client.headers["X-Forwarded-Email"] = "alice"
138
+ return client
139
+
140
+
141
+ def test_demo_ui_routes(monkeypatch):
142
+ client = _client(monkeypatch)
143
+
144
+ index = client.get("/")
145
+ assert index.status_code == 200
146
+ assert index.headers["cache-control"] == "no-store"
147
+ assert 'id="new-session"' in index.text
148
+ assert 'id="session-list"' in index.text
149
+ assert 'id="model-select"' in index.text
150
+ app_script = client.get("/ui-assets/app.js")
151
+ assert app_script.status_code == 200
152
+ assert app_script.headers["cache-control"] == "no-store"
153
+ assert "ab memory bind <store-name>" in app_script.text
154
+ assert "refreshSessionView({ hydrateChat: true })" in app_script.text
155
+ assert "function renderModels(" in app_script.text
156
+ assert 'demoUrl("/api/ui/config")' in app_script.text
157
+ assert 'demoUrl("/api/demo/models")' in app_script.text
158
+ assert 'fetch("/api/session/new"' not in app_script.text
159
+ assert "/api/demo/sessions/${encodeURIComponent(sessionId)}/open" in app_script.text
160
+ assert "session_id: sessionId" in app_script.text
161
+ assert 'fetch("/api/invocations"' in app_script.text
162
+ styles = client.get("/ui-assets/styles.css").text
163
+ assert "@media (min-width: 1181px)" in styles
164
+ assert "scrollbar-gutter: stable" in styles
165
+
166
+ config = client.get("/api/ui/config").json()
167
+ assert config["session_id"] == "routing-session"
168
+ assert config["deployed"] is False
169
+ assert config["models"] == {
170
+ "default": "system.ai.claude-sonnet-4-5",
171
+ "available": ["system.ai.claude-sonnet-4-5"],
172
+ }
173
+ assert config["streaming"]["enabled"] is True
174
+ assert config["background"]["enabled"] is True
175
+ assert config["streaming"]["persistent"] is False
176
+ assert config["streaming"]["mode"] == "In-process Runtime Store"
177
+ assert config["background"]["persistent"] is False
178
+ assert config["background"]["mode"] == "In-process Runtime Store"
179
+ assert config["memory"]["enabled"] is False
180
+ assert config["session"]["managed"] is False
181
+ assert config["session"]["history"] is True
182
+ assert config["session"]["mode"] == "In-process session"
183
+ assert "durability" not in config
184
+ assert "recovery" not in config
185
+ assert client.get("/api/demo/config").status_code == 404
186
+
187
+ assert client.get("/api/demo/models").json() == {
188
+ "default": "system.ai.claude-sonnet-4-5",
189
+ "available": ["system.ai.claude-sonnet-4-5"],
190
+ }
191
+
192
+ sessions = client.get("/api/demo/sessions").json()
193
+ assert sessions == {
194
+ "sessions": [
195
+ {
196
+ "session_id": "routing-session",
197
+ "actor_id": "agent",
198
+ "metadata": {"client": "agentbricks-demo-ui-local"},
199
+ }
200
+ ],
201
+ "current_session_id": "routing-session",
202
+ "managed": False,
203
+ }
204
+
205
+ assert client.post("/api/demo/memory/search", json={"query": "profile"}).status_code == 503
206
+ assert client.post("/api/demo/sessions", json={"session_id": "ignored"}).status_code == 503
207
+
208
+
209
+ @pytest.mark.parametrize(
210
+ ("local", "endpoint", "schema", "persistent"),
211
+ [
212
+ (None, None, None, False),
213
+ ("true", None, None, False),
214
+ ("true", "endpoint", "runtime_schema", False),
215
+ ("TRUE", "endpoint", "runtime_schema", False),
216
+ ("TrUe", "endpoint", "runtime_schema", False),
217
+ (None, "endpoint", "runtime_schema", True),
218
+ ("false", "endpoint", "runtime_schema", True),
219
+ (None, "endpoint", None, False),
220
+ (None, None, "runtime_schema", False),
221
+ (None, "", "runtime_schema", False),
222
+ (None, "endpoint", "", False),
223
+ ],
224
+ )
225
+ def test_demo_config_reports_runtime_store_configuration(
226
+ monkeypatch, local, endpoint, schema, persistent
227
+ ):
228
+ for name, value in (
229
+ (RUNTIME_STORE_LOCAL_ENV, local),
230
+ (RUNTIME_STORE_LAKEBASE_ENDPOINT_ENV, endpoint),
231
+ (RUNTIME_STORE_SCHEMA_ENV, schema),
232
+ ):
233
+ if value is not None:
234
+ monkeypatch.setenv(name, value)
235
+ client = _client(monkeypatch)
236
+
237
+ response = client.get("/api/ui/config")
238
+ assert response.status_code == 200
239
+ config = response.json()
240
+ for capability in ("streaming", "background"):
241
+ assert config[capability]["enabled"] is True
242
+ assert config[capability]["persistent"] is persistent
243
+ assert config[capability]["mode"] == (
244
+ "Runtime Store" if persistent else "In-process Runtime Store"
245
+ )
246
+
247
+
248
+ def test_demo_config_reports_managed_runtime_store(monkeypatch):
249
+ monkeypatch.setenv(RUNTIME_STORE_LAKEBASE_BRANCH_ENV, "projects/p/branches/b")
250
+ monkeypatch.setenv(RUNTIME_STORE_DATABASE_ENV, "runtime-db")
251
+ monkeypatch.setenv(RUNTIME_STORE_USERNAME_ENV, "app-sp")
252
+
253
+ config = _client(monkeypatch).get("/api/ui/config").json()
254
+
255
+ assert config["background"]["persistent"] is True
256
+
257
+
258
+ def test_demo_config_distinguishes_run_local_from_a_deployed_app(monkeypatch):
259
+ monkeypatch.setenv("DATABRICKS_APP_NAME", "app")
260
+ monkeypatch.setenv("DATABRICKS_APP_URL", "http://127.0.0.1:8000")
261
+ assert _client(monkeypatch).get("/api/ui/config").json()["deployed"] is False
262
+
263
+ monkeypatch.setenv("DATABRICKS_APP_URL", "https://agent.example.databricksapps.com")
264
+ assert _client(monkeypatch).get("/api/ui/config").json()["deployed"] is True
265
+
266
+
267
+ def test_demo_config_does_not_wait_for_model_discovery(monkeypatch):
268
+ client = _client(monkeypatch)
269
+ calls = []
270
+ monkeypatch.setattr(
271
+ ui,
272
+ "_discover_chat_models",
273
+ lambda: calls.append(True) or ["system.ai.claude-sonnet-4-5", "system.ai.llama-4-maverick"],
274
+ )
275
+
276
+ assert client.get("/api/ui/config").status_code == 200
277
+ assert calls == []
278
+ assert client.get("/api/demo/models").json()["available"] == [
279
+ "system.ai.claude-sonnet-4-5",
280
+ "system.ai.llama-4-maverick",
281
+ ]
282
+ assert calls == [True]
283
+
284
+
285
+ def test_unmanaged_local_history_route(monkeypatch):
286
+ client = _client(monkeypatch, history=True, session_id="local-session")
287
+
288
+ config = client.get("/api/ui/config").json()
289
+ assert config["session"]["managed"] is False
290
+ assert config["session"]["history"] is True
291
+
292
+ result = client.get("/api/demo/session/items")
293
+ assert result.status_code == 200
294
+ assert [item["data"]["content"] for item in result.json()["session_items"]] == [
295
+ "local-session",
296
+ "in-process reply",
297
+ ]
298
+
299
+
300
+ def test_managed_session_list_is_actor_scoped(monkeypatch):
301
+ monkeypatch.setenv("AGENT_SESSION_STORE", "sessions")
302
+ state_client = object.__new__(ui._ManagedStateClient)
303
+ calls = []
304
+ state_client._do = lambda method, path, **kwargs: calls.append((method, path, kwargs)) or {
305
+ "sessions": []
306
+ }
307
+
308
+ # The actor (a signed-in user) is escaped into the list filter.
309
+ assert state_client.list_sessions('alice "demo"') == {"sessions": []}
310
+ assert calls == [
311
+ (
312
+ "GET",
313
+ "/api/2.0/agents/session-stores/sessions/sessions",
314
+ {
315
+ "query": {
316
+ "filter": 'actor_id = "alice \\"demo\\""',
317
+ "order_by": "last_activity_time desc",
318
+ "page_size": 50,
319
+ }
320
+ },
321
+ )
322
+ ]
323
+
324
+
325
+ def test_chat_session_items_exclude_non_message_items():
326
+ result = ui._chat_session_items(
327
+ {
328
+ "session_items": [
329
+ {"item_id": "1", "data": {"role": "user", "content": "hello"}},
330
+ {"item_id": "2", "data": {"type": "ai", "content": "hi"}},
331
+ {"item_id": "3", "data": {"event_type": "checkpoint"}},
332
+ {"item_id": "5", "data": {"content": "missing role"}},
333
+ ],
334
+ "next_page_token": "next",
335
+ }
336
+ )
337
+
338
+ assert result == {
339
+ "session_items": [
340
+ {"item_id": "1", "data": {"role": "user", "content": "hello"}},
341
+ {"item_id": "2", "data": {"type": "ai", "content": "hi"}},
342
+ ],
343
+ "next_page_token": "next",
344
+ }
345
+
346
+
347
+ def test_discover_chat_models_pins_default_and_dedups(monkeypatch):
348
+ monkeypatch.setattr(ui, "_default_model", lambda: "system.ai.claude-sonnet-4-5")
349
+ monkeypatch.setattr(
350
+ ui,
351
+ "list_ai_gateway_model_services",
352
+ lambda _client: [
353
+ "system.ai.llama-4-maverick",
354
+ "system.ai.claude-sonnet-4-5",
355
+ "system.ai.claude-opus-4-8",
356
+ ],
357
+ )
358
+ monkeypatch.setattr(ui, "workspace_client", lambda: object())
359
+
360
+ # Default pinned first, the rest alphabetical, and the default not repeated by discovery.
361
+ assert ui._discover_chat_models() == [
362
+ "system.ai.claude-sonnet-4-5",
363
+ "system.ai.claude-opus-4-8",
364
+ "system.ai.llama-4-maverick",
365
+ ]
366
+
367
+
368
+ def test_discover_chat_models_falls_back_to_default_on_error(monkeypatch):
369
+ monkeypatch.setattr(ui, "_default_model", lambda: "system.ai.claude-sonnet-4-5")
370
+
371
+ def _boom(_client):
372
+ raise PermissionError("cannot read system.ai")
373
+
374
+ monkeypatch.setattr(ui, "list_ai_gateway_model_services", _boom)
375
+ monkeypatch.setattr(ui, "workspace_client", lambda: object())
376
+
377
+ # A workspace that can't list the gateway still gets a working picker.
378
+ assert ui._discover_chat_models() == ["system.ai.claude-sonnet-4-5"]
379
+
380
+
381
+ def test_discover_chat_models_caps_the_picker(monkeypatch):
382
+ monkeypatch.setattr(ui, "_default_model", lambda: "system.ai.claude-sonnet-4-5")
383
+ monkeypatch.setattr(
384
+ ui,
385
+ "list_ai_gateway_model_services",
386
+ # More services than the cap allows; synthetic because no real catalog is this long.
387
+ lambda _client: [f"system.ai.test-model-{i:03d}" for i in range(30)],
388
+ )
389
+ monkeypatch.setattr(ui, "workspace_client", lambda: object())
390
+
391
+ result = ui._discover_chat_models()
392
+ assert len(result) == ui._MODEL_LIMIT
393
+ assert result[0] == "system.ai.claude-sonnet-4-5" # the default survives truncation
394
+
395
+
396
+ @pytest.mark.asyncio
397
+ async def test_local_history_reads_messages_from_in_process_session(monkeypatch):
398
+ import databricks_agentkit.openai.sessions as ss
399
+
400
+ class _FakeSession:
401
+ async def get_items(self, limit=None):
402
+ return [
403
+ {"id": "m1", "role": "user", "content": "saved message"},
404
+ {"role": "assistant", "content": "saved reply"},
405
+ ]
406
+
407
+ monkeypatch.setattr(ss, "session_store", lambda session_id: _FakeSession())
408
+ result = await ui._local_history("saved-session")
409
+
410
+ assert result == {
411
+ "session_id": "saved-session",
412
+ "session_items": [
413
+ {"item_id": "m1", "data": {"id": "m1", "role": "user", "content": "saved message"}},
414
+ {"item_id": "1", "data": {"role": "assistant", "content": "saved reply"}},
415
+ ],
416
+ "interrupts": [],
417
+ }
418
+
419
+
420
+ def test_managed_memory_and_session_routes(monkeypatch):
421
+ client = _client(monkeypatch, configured=True, session_id="s1")
422
+
423
+ config = client.get("/api/ui/config").json()
424
+ assert config["memory"] == {
425
+ "enabled": True,
426
+ "store": "memory-stores/store",
427
+ "actor": "alice",
428
+ }
429
+ assert config["session"]["store"] == "sessions"
430
+ assert config["session"]["actor"] == "alice"
431
+ assert config["session"]["history"] is True
432
+
433
+ created = client.post(
434
+ "/api/demo/memory/entries",
435
+ json={"path": "/profile.md", "content": "I work at Databricks"},
436
+ )
437
+ assert created.status_code == 200
438
+ assert created.json()["path"] == "/profile.md"
439
+ assert created.json()["session_id"] == "s1"
440
+ assert client.get("/api/demo/memory/entries", params={"path_prefix": "/"}).status_code == 200
441
+ search = client.post("/api/demo/memory/search", json={"query": "Databricks"})
442
+ assert search.json()["managed_memory_entries"][0]["content"] == "Databricks"
443
+
444
+ # The UI can browse another actor's memories via ?actor= (list) or payload.actor (search);
445
+ # both default to the viewer when omitted.
446
+ listed_default = client.get("/api/demo/memory/entries").json()
447
+ assert listed_default["managed_memory_entries"][0]["actor_id"] == "alice"
448
+ listed_bob = client.get("/api/demo/memory/entries", params={"actor": "bob"}).json()
449
+ assert listed_bob["managed_memory_entries"][0]["actor_id"] == "bob"
450
+ search_bob = client.post("/api/demo/memory/search", json={"query": "x", "actor": "bob"})
451
+ assert search_bob.json()["managed_memory_entries"][0]["actor_id"] == "bob"
452
+
453
+ # Tracing surfaces in config when a destination + experiment are set, with an experiment link.
454
+ monkeypatch.setenv("MLFLOW_TRACKING_URI", "databricks")
455
+ monkeypatch.setenv("MLFLOW_EXPERIMENT_ID", "123456")
456
+ monkeypatch.setattr(ui, "_workspace_host", lambda: "https://example.databricks.com")
457
+ tracing = client.get("/api/ui/config").json()["tracing"]
458
+ assert tracing["enabled"] is True
459
+ assert tracing["url"] == "https://example.databricks.com/ml/experiments/123456"
460
+ monkeypatch.delenv("MLFLOW_TRACKING_URI", raising=False)
461
+ monkeypatch.delenv("MLFLOW_EXPERIMENT_ID", raising=False)
462
+ assert client.get("/api/ui/config").json()["tracing"]["enabled"] is False
463
+
464
+ assert (
465
+ client.post("/api/demo/sessions", json={"session_id": "ignored"}).json()["session_id"]
466
+ == "s1"
467
+ )
468
+ listed = client.get("/api/demo/sessions").json()
469
+ assert [session["session_id"] for session in listed["sessions"]] == ["s1", "s2"]
470
+ assert listed["current_session_id"] == "s1"
471
+ assert listed["managed"] is True
472
+ assert client.get("/api/demo/session").json()["session_id"] == "s1"
473
+ appended = client.post(
474
+ "/api/demo/session/items",
475
+ json={"items": [{"role": "user", "content": "hello"}]},
476
+ )
477
+ assert appended.json()["session_items"][0]["data"]["content"] == "hello"
478
+ assert (
479
+ client.get("/api/demo/session/items").json()["session_items"][0]["data"]["content"] == "s1"
480
+ )
481
+ assert [
482
+ item["data"]["content"]
483
+ for item in client.get("/api/demo/session/items").json()["session_items"]
484
+ ] == ["s1", "saved reply"]
485
+
486
+ opened = client.post("/api/demo/sessions/s2/open")
487
+ assert opened.json() == {
488
+ "session_id": "s2",
489
+ "previous_session_id": "s1",
490
+ "managed": True,
491
+ }
492
+ assert client.get("/api/ui/config", params={"session_id": "s2"}).json()["session_id"] == "s2"
493
+ assert (
494
+ client.get("/api/demo/session/items", params={"session_id": "s2"}).json()["session_items"][
495
+ 0
496
+ ]["data"]["content"]
497
+ == "s2"
498
+ )
499
+
500
+
501
+ def test_open_session_rejects_another_actor(monkeypatch):
502
+ client = _client(monkeypatch, configured=True, session_id="s1")
503
+
504
+ class _ForeignActorClient(_FakeStateClient):
505
+ def get_session(self, session_id):
506
+ return {"session_id": session_id, "actor_id": "bob"}
507
+
508
+ monkeypatch.setattr(ui, "_state_client", lambda: _ForeignActorClient())
509
+
510
+ response = client.post("/api/demo/sessions/s2/open")
511
+ assert response.status_code == 403
512
+ assert response.json()["detail"] == "Session belongs to another actor."