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,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))
|