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